├── .gitignore ├── FacileMenu.pro ├── LICENSE ├── README.md ├── facile_menu ├── facilemenu.cpp ├── facilemenu.h ├── facilemenuanimation.h ├── facilemenubar.cpp ├── facilemenubar.h ├── facilemenubarinterface.h ├── facilemenuitem.cpp └── facilemenuitem.h ├── interactive_buttons ├── interactivebuttonbase.cpp ├── interactivebuttonbase.h ├── watercirclebutton.cpp └── watercirclebutton.h ├── main.cpp ├── mainwindow.cpp ├── mainwindow.h ├── mainwindow.ui ├── resources.qrc ├── resources └── icons │ ├── faster.png │ ├── pause.png │ ├── resume.png │ ├── run.png │ ├── slower.png │ ├── stop.png │ └── sub_menu_arrow.png └── screenshots ├── menubar.gif └── picture.gif /.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 | -------------------------------------------------------------------------------- /FacileMenu.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 += interactive_buttons/ \ 19 | facile_menu/ 20 | 21 | SOURCES += \ 22 | facile_menu/facilemenu.cpp \ 23 | facile_menu/facilemenubar.cpp \ 24 | facile_menu/facilemenuitem.cpp \ 25 | interactive_buttons/interactivebuttonbase.cpp \ 26 | main.cpp \ 27 | mainwindow.cpp 28 | 29 | HEADERS += \ 30 | facile_menu/facilemenu.h \ 31 | facile_menu/facilemenuanimation.h \ 32 | facile_menu/facilemenubar.h \ 33 | facile_menu/facilemenubarinterface.h \ 34 | facile_menu/facilemenuitem.h \ 35 | interactive_buttons/interactivebuttonbase.h \ 36 | mainwindow.h 37 | 38 | FORMS += \ 39 | mainwindow.ui 40 | 41 | # Default rules for deployment. 42 | qnx: target.path = /tmp/$${TARGET}/bin 43 | else: unix:!android: target.path = /opt/$${TARGET}/bin 44 | !isEmpty(target.path): INSTALLS += target 45 | 46 | RESOURCES += \ 47 | resources.qrc 48 | 49 | DISTFILES += \ 50 | README.md \ 51 | screenshots/menubar.gif \ 52 | screenshots/picture.gif 53 | -------------------------------------------------------------------------------- /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 | FacileMenu 2 | ==== 3 | 4 | ## 介绍 5 | 6 | 非常飘逸的 Qt 菜单控件,带有各种动画效果,用起来也十分方便。 7 | 8 | 无限层级,响应键盘、鼠标单独操作,支持单快捷键。 9 | 10 | 允许添加自定义 widget、layout,当做特殊的 QDialog 使用。 11 | 12 | 13 | 14 | ## 简单使用 15 | 16 | 1. 放入源代码 17 | 将 `facile_menu` 文件夹放入 Qt 程序,pro 文件的 `INCLUDEPATH` 加上对应路径,`resources` 里的资源文件 `sub_menu_arrow.png` (子菜单箭头)也导入,前缀别名为:`:/icons/sub_menu_arrow`(或按需修改) 18 | 19 | 2. 包含头文件 20 | `#include "facile_menu.h"` 21 | 22 | 3. 创建并显示菜单 23 | 24 | ```C++ 25 | // 创建菜单 26 | FacileMenu* menu = new FacileMenu(this); 27 | 28 | // 添加动作 29 | menu->addAction(QIcon(":/icons/run"), "开始播放 (&S)", [=]{ 30 | /* 某操作 */ 31 | })->tip("Ctrl+S")->disable()->hide(); 32 | 33 | // 显示菜单 34 | menu->execute(QCursor::pos()); 35 | ``` 36 | 37 | 38 | 39 | 40 | ## 常用操作 41 | 42 | ### 连续设置 43 | 44 | ```C++ 45 | menu->addAction(QIcon(":/icons/run"), "开始播放 (&S)", [=]{}) 46 | ->tip("Ctrl+S") 47 | ->disable(playing/*如果满足某条件(默认true)则disable,不满足跳过,下同*/) 48 | ->hide(/*条件表达式*/) 49 | ->uncheck(false); 50 | ``` 51 | 52 | 53 | 54 | ### 子菜单 55 | 56 | ```C++ 57 | auto subMenu = menu->addMenu("子菜单2"); 58 | 59 | subMenu->addAction("继续", [=]{}); 60 | 61 | subMenu3->addAction("停止", [=]{}) 62 | ->disable(!playing); 63 | ``` 64 | 65 | 66 | 67 | ### 横向菜单 68 | 69 | 方式一:一口气添加 70 | 71 | ```C++ 72 | menu->addRow([=]{ 73 | menu->addAction("按钮1"); 74 | menu->addAction("按钮2"); 75 | menu->addAction("按钮3"); 76 | }); 77 | ``` 78 | 79 | 方式二:逐个添加 80 | 81 | ```C++ 82 | menu->beginRow(); 83 | menu->addAction(QIcon(":/icons/run")); 84 | menu->addAction(QIcon(":/icons/pause")); 85 | menu->split(); 86 | menu->addAction(QIcon(":/icons/resume")); 87 | menu->addAction(QIcon(":/icons/stop"))->disable(); 88 | menu->endRow(); 89 | ``` 90 | 91 | 两种方式都支持横向布局 widget 92 | 93 | 94 | 95 | ### 添加标题 96 | 97 | ```C++ 98 | menu->addTitle("标题", -1/0/1); 99 | ``` 100 | 101 | 一个灰色文字的 QLabel,根据参数二会选择性添加一条分割线。 102 | 103 | `-1` 添加到标题上方(margin=4),`0` 不添加分割线,`1` 添加到标题下方。默认为 `0`,不带分割线。 104 | 105 | 106 | 107 | ### 添加 QAction 108 | 109 | 支持在菜单关闭时自动 delete 传入的 action,避免内存泄漏(默认关闭) 110 | 111 | ```C++ 112 | QAction* action = ...; 113 | menu->addAction(action, true/*是否在菜单关闭时一起delete*/); 114 | ``` 115 | 116 | 117 | 118 | ### 添加 Widget/Layout 119 | 120 | 添加任意 widget 至菜单中,和菜单项并存,不占 `at(index)/indexOf(item)` 的位置。layout 同理。 121 | 122 | ```C++ 123 | QPushButton* button = new QPushButton("外部添加的按钮", this); 124 | menu->addWidget(button); 125 | ``` 126 | 127 | 128 | 129 | ### 添加单选菜单 130 | 131 | > 如果要设置为checkable,请在创建时调用一次其以下任一方法: 132 | > 133 | > setCheckable(bool) / setChecked(bool) / check(bool) / uncheck(bool) 134 | 135 | ```C++ 136 | // 使用 linger() 使菜单点击后不隐藏,持续显示当前单选/多选结果 137 | auto ac1 = subMenu2->addAction(QIcon(":/icons/run"), "带图标")->check()->linger(); 138 | auto ac2 = subMenu2->addAction("无图标")->uncheck()->linger(); 139 | auto ac3 = subMenu2->split()->addAction("全不选")->uncheck()->linger(); 140 | 141 | // 连接点击事件 142 | ac1->triggered([=]{ 143 | subMenu2->singleCheck(ac1); // 用于单选,表示只选中ac1 144 | // 这里可以用于处理其他操作 145 | }); 146 | ac2->triggered([=]{ 147 | subMenu2->singleCheck(ac2); 148 | }); 149 | ac3->triggered([=]{ 150 | subMenu2->uncheckAll(); // 全不选 151 | }); 152 | ``` 153 | 154 | 155 | 156 | ### 添加多选菜单 157 | 158 | ```C++ 159 | // 假装是某一个需要多选的属性 160 | QList* list = new QList(); 161 | 162 | for (int i = 0; i < 10; i++) 163 | { 164 | auto action = subMenu5->addAction("选项"+QString::number(i))->uncheck()->linger()->autoToggle()/*点击自动切换选中状态*/; 165 | action->triggered([=]{ 166 | // 自己的处理流程,例如调用某个外部的方法 167 | if (action->isChecked()) 168 | list->append(action->getText()); 169 | else 170 | list->removeOne(action->getText()); 171 | qDebug() << "当前选中的有:" << *list; 172 | }); 173 | } 174 | ``` 175 | 176 | 177 | 178 | ### 快速批量单选项 179 | 180 | ```C++ 181 | QStringList texts; 182 | for (int i = 0; i < 10; i++) 183 | texts << "项目"+QString::number(i); 184 | static int selected = 2; 185 | 186 | menu->addOptions(texts, selected, [=](int index){ 187 | qDebug() << "选中了:" << (selected = index) << texts.at(index); 188 | }); 189 | ``` 190 | 191 | 192 | 193 | ### 快速批量多选项 194 | 195 | 监听每一项改变的结果 196 | 197 | ```C++ 198 | // 假装是某一个需要多选的属性 199 | QList* list = new QList(); 200 | subMenu6->addNumberedActions("选项%1", 0, 10) 201 | ->setMultiCheck([=](int index, bool checked){ 202 | if (checked) 203 | list->append(index); 204 | else 205 | list->removeOne(index); 206 | qDebug() << "当前选中的有:" << *list; 207 | }); 208 | ``` 209 | 210 | 211 | 212 | ### 极简批量多选项 213 | 214 | 直接读取多选项结果,而不是监听多选项每一项(也可以两者结合) 215 | 216 | 在`finished()`中获取`checkedItems()`,即为选中项 217 | 218 | ```C++ 219 | QList* list = new QList(); 220 | subMenu7->addNumberedActions("选项%1", 0, 15)->setMultiCheck() 221 | ->finished([=]{ 222 | *list = subMenu7->checkedItemTexts(); 223 | qDebug() << "最终选中:" << *list; 224 | }); 225 | ``` 226 | 227 | 228 | 229 | ### 菜单项 API 230 | 231 | `addAction()`后,可直接设置菜单项的一些属性,包括以下: 232 | 233 | > 第一个参数为`bool`类型的,表示**满足此条件才修改设置**,例如: 234 | > 235 | > ```C++ 236 | > bool needHide = false; 237 | > action->hide(needHide); // 不满足隐藏条件,即无视此语句 238 | > ``` 239 | 240 | ```C++ 241 | // 菜单项右边快捷键区域的文字 242 | // 如果要使用,建议用:setTipArea 来额外添加设置右边空白宽度 243 | FacileMenuItem* tip(QString sc); 244 | FacileMenuItem* tip(bool exp, QString sc); 245 | 246 | // 鼠标悬浮提示 247 | FacileMenuItem* tooltip(QString tt); 248 | FacileMenuItem* tooltip(bool exp, QString tt); 249 | 250 | // 触发(单击、回车键)后,参数为 Lambda 表达式 251 | FacileMenuItem* triggered(FuncType func); 252 | FacileMenuItem* triggered(bool exp, FuncType func); 253 | 254 | // 当参数表达式为true时生效,false时忽略,下同 255 | FacileMenuItem* disable(bool exp = true); 256 | FacileMenuItem* enable(bool exp = true); 257 | 258 | FacileMenuItem* hide(bool exp = true); 259 | FacileMenuItem* visible(bool exp = true); 260 | 261 | FacileMenuItem* check(bool exp = true); 262 | FacileMenuItem* uncheck(bool exp = true); 263 | FacileMenuItem* toggle(bool exp = true); 264 | 265 | // 设置data,一般用于单选、多选 266 | FacileMenuItem* setData(QVariant data); 267 | QVariant getData(); 268 | 269 | FacileMenuItem* text(bool exp, QString str); 270 | // 当表达式为true时,设置为tru文字,否则设置为fal文字 271 | FacileMenuItem* text(bool exp, QString tru, QString fal); 272 | 273 | FacileMenuItem* fgColor(QColor color); 274 | FacileMenuItem* fgColor(bool exp, QColor color); 275 | 276 | FacileMenuItem* bgColor(QColor color); 277 | FacileMenuItem* bgColor(bool exp, QColor color); 278 | 279 | // 插入前缀 280 | FacileMenuItem* prefix(bool exp, QString pfix); 281 | FacileMenuItem* prefix(QString pfix); 282 | 283 | // 插入后缀,参数3支持类似 "action后缀 (K)" 这样的格式 284 | FacileMenuItem* suffix(bool exp, QString sfix, bool inLeftParenthesis = true); 285 | FacileMenuItem* suffix(QString sfix, bool inLeftParenthesis = true); 286 | 287 | FacileMenuItem* icon(bool ic, QIcon icon); 288 | 289 | // 设置边界:半径、颜色 290 | FacileMenuItem* borderR(int radius = 3, QColor co = Qt::transparent); 291 | 292 | // 点击后是否保持菜单显示(默认点一下就隐藏菜单) 293 | FacileMenuItem* linger(); 294 | // 点击后保持显示(同linger()),并且修改菜单项文本 295 | FacileMenuItem* lingerText(QString textAfterClick); 296 | 297 | // 点击后的菜单文本改变 298 | textAfterClick(QString newText); 299 | // 根据当前文本修改为新文本的 Lambda 表达式 300 | // 参数示例:[=](QString s) -> QString { if (s == "xx") return "xx"; } 301 | textAfterClick(FuncStringStringType func); 302 | 303 | // 满足 exp 时执行 trueLambda 表达式,否则执行 falseLambda 表达式 304 | FacileMenuItem* ifer(bool exp, trueLambda, falseLambda = nullptr); 305 | 306 | // 逻辑控制 307 | FacileMenuItem* ifer(bool exp); // 满足条件时才继续,下同 308 | FacileMenuItem* elifer(bool exp); 309 | FacileMenuItem* elser(); 310 | 311 | FacileMenuItem* switcher(int value); 312 | FacileMenuItem* caser(int value, matchedLambda); // 匹配时执行Lambda,无需break 313 | FacileMenuItem* caser(int value); // 结束记得breaker(允许忘掉~) 314 | FacileMenuItem* breaker(); 315 | FacileMenuItem* defaulter(); 316 | 317 | // 取消后面所有命令(无视层级,相当于函数中return) 318 | FacileMenuItem* exiter(bool ex = true); 319 | ``` 320 | 321 | 322 | > 注意:由于加了一些容错处理(例如caser可以不用写breaker),无法进行if/switch的多层嵌套(较多的逻辑运算不建议放在菜单中) 323 | 324 | 325 | 326 | ## 配置项 327 | 328 | ### 静态统一颜色 329 | 330 | 都是静态变量,设置一次,所有菜单都生效。 331 | 332 | ```C++ 333 | FacileMenu::normal_bg = QColor(255, 255, 255); 334 | FacileMenu::hover_bg = QColor(128, 128, 128, 64); 335 | FacileMenu::press_bg = QColor(128, 128, 128, 128); 336 | FacileMenu::text_fg = QColor(0, 0, 0); 337 | FacileMenu::blur_bg_alpha = DEFAULT_MENU_BLUR_ALPHA; 338 | ``` 339 | 340 | 341 | 342 | ### 单个菜单设置 343 | 344 | 一些可选的设置项,按需加上去,也可以都不加。 345 | 346 | ```C++ 347 | FacileMenu* menu = (new FacileMenu(this)) 348 | ->setTipArea("Ctrl+Alt+P") // 设置右边的快捷键提示区域的留空宽度,建议使用最长快捷键 349 | ->setSplitInRow(true) // 横向按钮是否使用分割线分隔 350 | ->setSubMenuShowOnCursor(false) // 设置子菜单是从鼠标位置出现还是右边出现 351 | ->setAppearAnimation(false) // 菜单出现动画 352 | ->setDisappearAnimation(false); // 菜单消失动画 353 | ``` 354 | 355 | 如果多级菜单中要将这些设置项传递给子菜单。 356 | 357 | 其中仅 `setTipArea` 不会传递给下一级。 358 | 359 | 可以直接修改源码中这些变量的默认值,全部菜单生效。 360 | 361 | 362 | 363 | ## 截图 364 | 365 | ![菜单](screenshots/picture.gif) 366 | 367 | 368 | 369 | ## 菜单栏 370 | 371 | ![菜单栏](screenshots/menubar.gif) 372 | 373 | 结合 FacileMenu 自定义的一个菜单栏,目前不是很完善(做着玩的,但是对这个动画效果确实挺失望的)。 374 | 375 | 具体可参考 MainWindow 中的写法: 376 | 377 | ```C++ 378 | ui->menuBar->setAnimationEnabled(false); // 开启动画 379 | ui->menuBar->addMenu("文件", fileMenu); 380 | ui->menuBar->addMenu("编辑", editMenu); 381 | ui->menuBar->addMenu("查看", viewMenu); 382 | ui->menuBar->addMenu("帮助", helpMenu); 383 | ui->menuBar->insertMenu(2, "格式", formatMenu); 384 | ``` 385 | 386 | 387 | 388 | ## 注意点 389 | 390 | ### 打开**模态对话框**可能会引起崩溃 391 | 392 | 需要在打开模态对话框之前,关闭当前 menu 393 | 394 | ```C++ 395 | menu->addAction("选择文件", [=]{ 396 | menu->close(); // 需要这句,否则会导致崩溃 397 | QString path = QFileDialog::getOpenFileName(this, "选择文件", prevPath); 398 | // ... 399 | }); 400 | ``` 401 | 402 | 403 | ### 菜单关闭导致退出程序 404 | 405 | 在 `main.cpp` 中添加以下代码,使窗口关闭后不会退出整个程序: 406 | 407 | ```C++ 408 | QApplication a(argc, argv); 409 | a.setQuitOnLastWindowClosed(false); 410 | ``` 411 | -------------------------------------------------------------------------------- /facile_menu/facilemenu.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "facilemenu.h" 3 | 4 | QColor FacileMenu::normal_bg = QColor(255, 255, 255); 5 | QColor FacileMenu::hover_bg = QColor(128, 128, 128, 64); 6 | QColor FacileMenu::press_bg = QColor(128, 128, 128, 128); 7 | QColor FacileMenu::text_fg = QColor(0, 0, 0); 8 | int FacileMenu::blur_bg_alpha = DEFAULT_MENU_BLUR_ALPHA; 9 | QEasingCurve FacileMenu::easing_curve = QEasingCurve::OutBack; // OutCubic 也不错 10 | 11 | FacileMenu::FacileMenu(QWidget *parent) : QWidget(parent) 12 | { 13 | setObjectName("FacileMenu"); 14 | setAttribute(Qt::WA_DeleteOnClose, true); 15 | setFocusPolicy(Qt::StrongFocus); // 获取焦点,允许按键点击 16 | setWindowFlag(Qt::Popup, true); 17 | setAutoFillBackground(false); //这个不设置的话就背景变黑 18 | setAttribute(Qt::WA_StyledBackground); 19 | 20 | main_hlayout = new QHBoxLayout(this); 21 | main_vlayout = new QVBoxLayout(); 22 | setLayout(main_hlayout); 23 | main_hlayout->addLayout(main_vlayout); 24 | main_hlayout->setMargin(0); 25 | main_hlayout->setSpacing(0); 26 | 27 | setStyleSheet("#FacileMenu { background: "+QVariant(normal_bg).toString()+"; border: none; border-radius:5px; }"); 28 | 29 | setMouseTracking(true); 30 | 31 | // 获取窗口尺寸 32 | window_rect = QApplication::desktop()->availableGeometry(); 33 | window_height = window_rect.height(); 34 | } 35 | 36 | /** 37 | * 这是临时菜单,并不显示出来 38 | * 只用来进行各种暗中的逻辑运算 39 | */ 40 | FacileMenu::FacileMenu(bool, QWidget *parent) : FacileMenu(parent) 41 | { 42 | setAttribute(Qt::WA_DeleteOnClose, false); 43 | setFocusPolicy(Qt::NoFocus); 44 | 45 | // 保存父菜单的指针,判断鼠标移动的位置 46 | parent_menu = static_cast(parent); 47 | } 48 | 49 | FacileMenu::~FacileMenu() 50 | { 51 | if (finished_func) 52 | { 53 | (*finished_func)(); 54 | delete finished_func; 55 | } 56 | foreach (auto action, import_actions) 57 | action->deleteLater(); 58 | } 59 | 60 | FacileMenuItem *FacileMenu::addAction(QIcon icon, QString text, FuncType clicked) 61 | { 62 | auto item = createMenuItem(icon, text); 63 | connect(item, &InteractiveButtonBase::clicked, this, [=]{ 64 | if (_showing_animation) 65 | return ; 66 | if (clicked != nullptr) 67 | clicked(); 68 | if (item->isLinger()) 69 | return ; 70 | emit signalActionTriggered(item); 71 | toHide(items.indexOf(item)); 72 | }); 73 | connect(item, &InteractiveButtonBase::signalMouseEnter, this, [=]{ itemMouseEntered(item); }); 74 | return item; 75 | } 76 | 77 | FacileMenuItem *FacileMenu::addAction(QIcon icon, FuncType clicked) 78 | { 79 | return addAction(icon, "", clicked); 80 | } 81 | 82 | FacileMenuItem *FacileMenu::addAction(QString text, FuncType clicked) 83 | { 84 | return addAction(QIcon(), text, clicked); 85 | } 86 | 87 | FacileMenuItem *FacileMenu::addAction(QAction *action, bool deleteWithMenu) 88 | { 89 | if (deleteWithMenu) 90 | import_actions.append(action); // 加入列表,菜单delete时一起删了 91 | if (action->menu() != nullptr) // 这是个菜单 92 | { 93 | auto menu = addMenu(action->icon(), action->text(), [=]{ action->trigger(); }); 94 | menu->addActions(action->menu()->actions()); 95 | return menu->parentAction(); 96 | } 97 | else // 普通的action 98 | { 99 | auto ac = addAction(action->icon(), action->text(), [=]{ action->trigger(); }); 100 | if (action->isChecked()) 101 | ac->check(); 102 | if (!action->toolTip().isEmpty()) 103 | ac->tooltip(action->toolTip()); 104 | return ac; 105 | } 106 | } 107 | 108 | /** 109 | * 回调:类内方法(学艺不精,用不来……) 110 | */ 111 | template 112 | FacileMenuItem *FacileMenu::addAction(QIcon icon, QString text, T *obj, void (T::*func)()) 113 | { 114 | auto item = createMenuItem(icon, text); 115 | connect(item, &InteractiveButtonBase::clicked, this, [=]{ 116 | if (_showing_animation) 117 | return ; 118 | (obj->*func)(); 119 | if (item->isLinger()) 120 | return ; 121 | emit signalActionTriggered(item); 122 | toHide(items.indexOf(item)); 123 | }); 124 | connect(item, &InteractiveButtonBase::signalMouseEnter, this, [=]{ itemMouseEntered(item); }); 125 | return item; 126 | } 127 | 128 | /** 129 | * 批量添加带数字(可以不带)的action 130 | * 相当于只是少了个for循环…… 131 | * @param pattern 例如 项目%1 132 | * @param numberEnd 注意,结数值不包括结尾! 133 | */ 134 | FacileMenu *FacileMenu::addNumberedActions(QString pattern, int numberStart, int numberEnd, FuncItemType config, FuncIntType clicked, int step) 135 | { 136 | if (!step) 137 | step = numberStart <= numberEnd ? 1 : -1; 138 | for (int i = numberStart; i != numberEnd; i += step) 139 | { 140 | auto ac = addAction(pattern.arg(i), [=]{ 141 | if (clicked) 142 | clicked(i); 143 | }); 144 | if (config) 145 | config(ac); 146 | } 147 | return this; 148 | } 149 | 150 | /** 151 | * 同上 152 | * @param pattern 例如 项目%1 153 | * @param numberEnd 注意,结数值不包括结尾! 154 | * @param config (Item*, int) 其中参数2表示number遍历的位置,不是当前item的index 155 | */ 156 | FacileMenu *FacileMenu::addNumberedActions(QString pattern, int numberStart, int numberEnd, FuncItemIntType config, FuncIntType clicked, int step) 157 | { 158 | if (!step) 159 | step = numberStart <= numberEnd ? 1 : -1; 160 | for (int i = numberStart; i != numberEnd; i += step) 161 | { 162 | auto ac = addAction(pattern.arg(i), [=]{ 163 | if (clicked) 164 | clicked(i); 165 | }); 166 | if (config) 167 | config(ac, i); 168 | } 169 | return this; 170 | } 171 | 172 | FacileMenu *FacileMenu::addActions(QList actions) 173 | { 174 | foreach (auto action, actions) 175 | { 176 | addAction(action); 177 | } 178 | return this; 179 | } 180 | 181 | FacileMenu *FacileMenu::addRow(FuncType addActions) 182 | { 183 | QHBoxLayout* layout = new QHBoxLayout; 184 | row_hlayouts.append(layout); 185 | main_vlayout->addLayout(layout); 186 | 187 | align_mid_if_alone = true; 188 | adding_horizone = true; 189 | 190 | addActions(); 191 | 192 | align_mid_if_alone = false; 193 | adding_horizone = false; 194 | 195 | return this; 196 | } 197 | 198 | FacileMenu *FacileMenu::beginRow() 199 | { 200 | QHBoxLayout* layout = new QHBoxLayout; 201 | row_hlayouts.append(layout); 202 | main_vlayout->addLayout(layout); 203 | 204 | align_mid_if_alone = true; 205 | adding_horizone = true; 206 | 207 | return this; 208 | } 209 | 210 | FacileMenu *FacileMenu::endRow() 211 | { 212 | align_mid_if_alone = false; 213 | adding_horizone = false; 214 | 215 | return this; 216 | } 217 | 218 | QVBoxLayout *FacileMenu::createNextColumn() 219 | { 220 | main_vlayout = new QVBoxLayout; 221 | main_hlayout->addLayout(main_vlayout); 222 | main_vlayout->setAlignment(Qt::AlignLeft | Qt::AlignTop); 223 | return main_vlayout; 224 | } 225 | 226 | /** 227 | * 获取当前的layout 228 | * 如果是正在横向布局,则返回横向布局的layout(子) 229 | * 否则返回竖直布局的总layout 230 | */ 231 | QBoxLayout *FacileMenu::currentLayout() const 232 | { 233 | if (adding_horizone && row_hlayouts.size()) 234 | return row_hlayouts.last(); 235 | else 236 | return main_vlayout; 237 | } 238 | 239 | FacileMenu *FacileMenu::addTitle(QString text, int split) 240 | { 241 | if (split < 0) 242 | { 243 | this->split(); 244 | if (layout()->count()) 245 | this->addSpacing(4); // 因为是与上一个菜单项分隔,不能太紧凑,要加点空白 246 | } 247 | QLabel* label = new QLabel(text, this); 248 | label->setStyleSheet("margin: 4px; color: gray;"); 249 | addWidget(label); 250 | if (split > 0) 251 | this->split(); 252 | return this; 253 | } 254 | 255 | /** 256 | * 添加一项子菜单 257 | * 鼠标浮在上面展开 258 | * 同时也可以设置点击事件 259 | */ 260 | FacileMenu *FacileMenu::addMenu(QIcon icon, QString text, FuncType clicked) 261 | { 262 | auto item = createMenuItem(icon, text); 263 | 264 | // 子菜单项是否可点击 265 | if (clicked != nullptr) 266 | { 267 | connect(item, &InteractiveButtonBase::clicked, this, [=]{ 268 | if (_showing_animation) 269 | return ; 270 | 271 | clicked(); 272 | emit signalActionTriggered(item); 273 | toHide(items.indexOf(item)); 274 | }); 275 | } 276 | else // 点击出现子项 277 | { 278 | connect(item, &InteractiveButtonBase::clicked, this, [=]{ 279 | if (_showing_animation) 280 | return ; 281 | 282 | // 显示子菜单 283 | showSubMenu(item); 284 | }); 285 | } 286 | 287 | connect(item, &InteractiveButtonBase::signalMouseEnterLater, [=]{ 288 | if (_showing_animation) 289 | return ; 290 | int index = items.indexOf(item); 291 | if (using_keyboard && current_index > -1 && current_index < items.size() && current_index != index) // 屏蔽键盘操作 292 | items.at(current_index)->discardHoverPress(true); 293 | current_index = index; 294 | 295 | // 显示子菜单 296 | // 可能是需要点击这个菜单项,但是点下去隐藏子菜单,会再次触发 mouseEnterLater 事件 297 | // 需要判断位置,屏蔽第二次的 enter 事件,得以点击菜单项 298 | // 不过还是需要双击才行,第一次是隐藏子菜单,第二次才是真正点击 299 | QPoint showPos = mapFromGlobal(QCursor::pos()); 300 | if (_enter_later_pos == showPos) 301 | return ; 302 | 303 | _enter_later_pos = showPos; 304 | if (current_index == items.indexOf(item)) 305 | showSubMenu(item); 306 | }); 307 | 308 | // 创建菜单项 309 | FacileMenu* menu = new FacileMenu(true, this); 310 | menu->split_in_row = this->split_in_row; 311 | menu->enable_appear_animation = this->enable_appear_animation; 312 | menu->enable_disappear_animation = this->enable_disappear_animation; 313 | menu->sub_menu_show_on_cursor = this->sub_menu_show_on_cursor; 314 | menu->hide(); 315 | item->setSubMenu(menu); 316 | connect(menu, &FacileMenu::signalHidden, item, [=]{ 317 | if (!using_keyboard || current_index != items.indexOf(item)) 318 | { 319 | // 子菜单隐藏,当前按钮强制取消hover状态 320 | item->discardHoverPress(true); 321 | } 322 | 323 | // 如果是用户主动隐藏子菜单,那么就隐藏全部菜单 324 | // 有一种情况是需要点击这个菜单项而不是弹出的子菜单,需要避免 325 | if (!menu->hidden_by_another && !linger_on_submenu_clicked 326 | && !rect().contains(mapFromGlobal(QCursor::pos()))) // 允许鼠标浮在菜单项上,ESC关闭子菜单 327 | { 328 | this->hide(); // 隐藏自己,在隐藏事件中继续向上传递隐藏的信号 329 | } 330 | }); 331 | connect(menu, &FacileMenu::signalActionTriggered, this, [=](FacileMenuItem* action){ 332 | closed_by_clicked = true; 333 | // 子菜单被点击了,副菜单依次隐藏 334 | emit signalActionTriggered(action); 335 | if (!linger_on_submenu_clicked) 336 | toHide(items.indexOf(item)); 337 | }); 338 | return menu; 339 | } 340 | 341 | FacileMenu *FacileMenu::addMenu(QString text, FuncType clicked) 342 | { 343 | return addMenu(QIcon(), text, clicked); 344 | } 345 | 346 | /** 347 | * 从已有menu中导入actions 348 | */ 349 | FacileMenu *FacileMenu::addMenu(QMenu *menu) 350 | { 351 | FacileMenu* m = addMenu(menu->icon(), menu->title()); 352 | auto actions = menu->actions(); 353 | for (int i = 0; i < actions.size(); i++) 354 | { 355 | m->addAction(actions.at(i)); 356 | } 357 | return m; 358 | } 359 | 360 | /** 361 | * 子菜单所属的action 362 | */ 363 | FacileMenuItem *FacileMenu::parentAction() 364 | { 365 | if (!parent_menu) // 不是子菜单 366 | return nullptr; 367 | foreach (auto item, parent_menu->items) 368 | { 369 | if (item->isSubMenu() && item->subMenu() == this) 370 | return item; 371 | } 372 | return nullptr; 373 | } 374 | 375 | /** 376 | * 获取最后一个action 377 | * 不只是最后一个,更是正在编辑的这个 378 | * 如果添加了子菜单,那么就需要这个返回值 379 | */ 380 | FacileMenuItem *FacileMenu::lastAction() 381 | { 382 | if (items.size() == 0) 383 | return nullptr; 384 | return items.last(); 385 | } 386 | 387 | /** 388 | * 当前正在调整的菜单项 389 | * 当然是最后添加的那个 390 | */ 391 | FacileMenuItem *FacileMenu::currentAction() 392 | { 393 | return lastAction(); 394 | } 395 | 396 | FacileMenu *FacileMenu::addLayout(QLayout *layout, int stretch) 397 | { 398 | if (!layout) 399 | return this; 400 | if (adding_horizone && row_hlayouts.size()) // 如果是正在添加横向按钮 401 | { 402 | if (split_in_row && row_hlayouts.last()->count() > 0) 403 | addVSeparator(); // 添加竖向分割线 404 | row_hlayouts.last()->addLayout(layout, stretch); 405 | } 406 | else 407 | { 408 | main_vlayout->addLayout(layout, stretch); 409 | } 410 | for (int i = 0; i < layout->count(); i++) 411 | { 412 | auto it = layout->itemAt(i); 413 | auto widget = qobject_cast(it->widget()); 414 | if (widget) // 如果这个 LayoutItem 是 widget 415 | other_widgets.append(widget); 416 | } 417 | return this; 418 | } 419 | 420 | FacileMenu *FacileMenu::addLayoutItem(QLayoutItem* item) 421 | { 422 | currentLayout()->addItem(item); 423 | return this; 424 | } 425 | 426 | FacileMenu *FacileMenu::addSpacerItem(QSpacerItem *spacerItem) 427 | { 428 | currentLayout()->addSpacerItem(spacerItem); 429 | return this; 430 | } 431 | 432 | FacileMenu *FacileMenu::addSpacing(int size) 433 | { 434 | currentLayout()->addSpacing(size); 435 | return this; 436 | } 437 | 438 | FacileMenu *FacileMenu::addStretch(int stretch) 439 | { 440 | currentLayout()->addStretch(stretch); 441 | return this; 442 | } 443 | 444 | FacileMenu *FacileMenu::addStrut(int size) 445 | { 446 | currentLayout()->addStrut(size); 447 | return this; 448 | } 449 | 450 | FacileMenu *FacileMenu::addWidget(QWidget *widget, int stretch, Qt::Alignment alignment) 451 | { 452 | if (!widget) 453 | return this; 454 | QBoxLayout* layout = nullptr; 455 | if (adding_horizone && row_hlayouts.size()) // 如果是正在添加横向按钮 456 | { 457 | if (split_in_row && row_hlayouts.last()->count() > 0) 458 | addVSeparator(); // 添加竖向分割线 459 | layout = row_hlayouts.last(); 460 | } 461 | else 462 | { 463 | if (main_vlayout->sizeHint().height() + widget->height() > window_height) 464 | createNextColumn(); 465 | layout = main_vlayout; 466 | } 467 | layout->addWidget(widget, stretch, alignment); 468 | other_widgets.append(widget); 469 | return this; 470 | } 471 | 472 | FacileMenu *FacileMenu::setSpacing(int spacing) 473 | { 474 | currentLayout()->setSpacing(spacing); 475 | return this; 476 | } 477 | 478 | FacileMenu *FacileMenu::setStretchFactor(QWidget *widget, int stretch) 479 | { 480 | main_vlayout->setStretchFactor(widget, stretch); 481 | return this; 482 | } 483 | 484 | FacileMenu *FacileMenu::setStretchFactor(QLayout *layout, int stretch) 485 | { 486 | main_vlayout->setStretchFactor(layout, stretch); 487 | return this; 488 | } 489 | 490 | /** 491 | * 添加水平分割线 492 | * 不一定需要 493 | */ 494 | FacileMenuItem *FacileMenu::addSeparator() 495 | { 496 | if (adding_horizone) 497 | { 498 | if (!row_hlayouts.last()->count()) 499 | return nullptr; 500 | return addVSeparator(); 501 | } 502 | 503 | if (!main_vlayout->count()) 504 | return nullptr; 505 | 506 | FacileMenuItem* item = new FacileMenuItem(this); 507 | item->setNormalColor(QColor(64, 64, 64, 64)); 508 | item->setFixedHeight(1); 509 | item->setPaddings(32, 32, 0, 0); 510 | item->setDisabled(true); 511 | 512 | main_vlayout->addWidget(item); 513 | h_separators.append(item); 514 | last_added_item = item; 515 | 516 | return item; 517 | } 518 | 519 | FacileMenu *FacileMenu::split() 520 | { 521 | addSeparator(); 522 | return this; 523 | } 524 | 525 | /** 526 | * 返回最后添加的菜单项(包括分割线) 527 | */ 528 | FacileMenuItem *FacileMenu::lastAddedItem() 529 | { 530 | return last_added_item; 531 | } 532 | 533 | bool FacileMenu::hasFocus() const 534 | { 535 | if (QWidget::hasFocus()) 536 | return true; 537 | if (current_sub_menu && current_sub_menu->hasFocus()) 538 | return true; 539 | return false; 540 | } 541 | 542 | /** 543 | * 返回菜单项的索引 544 | * 不包括非菜单项的自定义控件、布局、分割线等 545 | * 未找到返回-1 546 | */ 547 | int FacileMenu::indexOf(FacileMenuItem *item) 548 | { 549 | return items.indexOf(item); 550 | } 551 | 552 | /** 553 | * 返回索引对应的菜单项 554 | * 不包括非菜单项的自定义控件、布局、分割线等 555 | * 未找到返回nullptr 556 | */ 557 | FacileMenuItem *FacileMenu::at(int index) 558 | { 559 | if (index < 0 || index >= items.size()) 560 | return nullptr; 561 | return items.at(index); 562 | } 563 | 564 | /** 565 | * 设置菜单栏 566 | * 鼠标移动时会增加判断是否移动到菜单栏按钮上 567 | */ 568 | void FacileMenu::setMenuBar(FacileMenuBarInterface *mb) 569 | { 570 | this->menu_bar = mb; 571 | } 572 | 573 | /** 574 | * 一行有多个按钮时的竖向分割线 575 | * 只有添加chip前有效 576 | */ 577 | FacileMenuItem *FacileMenu::addVSeparator() 578 | { 579 | FacileMenuItem* item = new FacileMenuItem(this); 580 | item->setNormalColor(QColor(64, 64, 64, 64)); 581 | item->setFixedWidth(1); 582 | item->setPaddings(0, 0, 0, 0); 583 | item->setDisabled(true); 584 | 585 | row_hlayouts.last()->addWidget(item); 586 | v_separators.append(item); 587 | last_added_item = item; 588 | 589 | return item; 590 | } 591 | 592 | /** 593 | * 在鼠标或指定点展开 594 | * 自动避开屏幕边缘 595 | */ 596 | void FacileMenu::exec(QPoint pos) 597 | { 598 | if (pos == QPoint(-1,-1)) 599 | pos = QCursor::pos(); 600 | QPoint originPos = pos; // 不包含像素偏移的原始点 601 | main_vlayout->setEnabled(true); 602 | main_vlayout->activate(); // 先调整所有控件大小 603 | this->adjustSize(); 604 | 605 | // setAttribute(Qt::WA_DontShowOnScreen); // 会触发 setMouseGrabEnabled 错误 606 | // show(); 607 | // hide(); // 直接显示吧 608 | // setAttribute(Qt::WA_DontShowOnScreen, false); 609 | 610 | int x = pos.x() + 1; 611 | int y = pos.y() + 1; 612 | int w = width() + 1; 613 | int h = height() + 1; 614 | QRect avai = window_rect; // 屏幕大小 615 | 616 | // 如果超过范围,则调整位置 617 | if (x + w > avai.right()) 618 | x = avai.right() - w; 619 | if (y + h > avai.bottom()) 620 | y = avai.bottom() - h; 621 | if (x >= w && pos.x() + w > avai.right()) 622 | x = originPos.x() - w; 623 | if (y >= h && pos.y() + h > avai.bottom()) 624 | y = originPos.y() - h; 625 | 626 | // 移动窗口 627 | move(QPoint(x, y)); 628 | 629 | execute(); 630 | } 631 | 632 | /** 633 | * 确保在指定矩形框之外展开 634 | * 注意:即使菜单项超过了窗口范围,也不会覆盖这个矩形 635 | * @param expt 这个是相对整个屏幕的坐标,用 mapToGlobal(geometry()) 636 | * @param vertical 优先左边对齐(出现在下边),还是顶部对齐(出现在右边) 637 | * @param pos 默认位置。如果在expt外且非边缘,则不受expt影响 638 | */ 639 | void FacileMenu::exec(QRect expt, bool vertical, QPoint pos) 640 | { 641 | if (pos == QPoint(-1,-1)) 642 | pos = QCursor::pos(); 643 | main_vlayout->setEnabled(true); 644 | main_hlayout->invalidate(); 645 | main_vlayout->activate(); // 先调整所有控件大小 646 | 647 | // setAttribute(Qt::WA_DontShowOnScreen); // 会触发 setMouseGrabEnabled 错误 648 | // show(); // 但直接显示会有一瞬间闪烁情况 649 | // hide(); 650 | // setAttribute(Qt::WA_DontShowOnScreen, false); 651 | 652 | // 根据 rect 和 avai 自动调整范围 653 | QRect avai = window_rect; 654 | QRect rect = geometry(); 655 | rect.moveTo(pos); 656 | if (!vertical) // 优先横向对齐(顶上) 657 | { 658 | if (rect.left() <= expt.right() && rect.right() > expt.right()) 659 | rect.moveLeft(expt.right()); 660 | rect.moveTop(expt.top()); 661 | 662 | // 避开屏幕位置 663 | if (expt.left() > rect.width() && rect.right() >= avai.right()) 664 | rect.moveLeft(expt.left() - rect.width()); 665 | if (rect.bottom() >= avai.bottom()) 666 | { 667 | if (expt.top() > rect.height()) 668 | rect.moveTop(expt.bottom() - rect.height()); 669 | else 670 | rect.moveTop(avai.bottom() - rect.height()); 671 | } 672 | } 673 | else // 优先纵向对齐(左下) 674 | { 675 | if (rect.top() <= expt.bottom() && rect.bottom() > expt.bottom()) 676 | rect.moveTop(expt.bottom()); 677 | rect.moveLeft(expt.left()); 678 | 679 | // 避开屏幕位置 680 | if (rect.right() >= avai.right()) 681 | { 682 | if (expt.left() > rect.width()) 683 | rect.moveLeft(expt.right() - rect.width()); 684 | else 685 | rect.moveLeft(avai.right() - rect.width()); 686 | } 687 | if (expt.top() > rect.height() && rect.bottom() >= avai.bottom()) 688 | rect.moveTop(expt.top() - rect.height()); 689 | } 690 | 691 | // 移动窗口 692 | move(rect.topLeft()); 693 | 694 | execute(); 695 | } 696 | 697 | void FacileMenu::execute() 698 | { 699 | current_index = -1; 700 | 701 | // 设置背景为圆角矩形 702 | if (height() > 0 && border_radius) // 没有菜单项的时候为0 703 | { 704 | QPixmap pixmap(width(), height()); 705 | pixmap.fill(Qt::transparent); 706 | QPainter pix_ptr(&pixmap); 707 | pix_ptr.setRenderHint(QPainter::Antialiasing, true); 708 | QPainterPath path; 709 | path.addRoundedRect(0, 0, width(), height(), border_radius, border_radius); 710 | pix_ptr.fillPath(path, Qt::white); 711 | setMask(pixmap.mask()); 712 | } 713 | 714 | // 是否捕获背景模糊图片 715 | if (blur_bg_alpha > 0) 716 | { 717 | // 获取图片 718 | QRect rect = this->geometry(); 719 | int radius = qMin(64, qMin(width(), height())); // 模糊半径,也是边界 720 | rect.adjust(-radius, -radius, +radius, +radius); 721 | QScreen* screen = QApplication::screenAt(QCursor::pos()); 722 | if (screen) 723 | { 724 | QPixmap bg = screen->grabWindow(QApplication::desktop()->winId(), rect.left(), rect.top(), rect.width(), rect.height()); 725 | 726 | // 开始模糊 727 | QT_BEGIN_NAMESPACE 728 | extern Q_WIDGETS_EXPORT void qt_blurImage( QPainter *p, QImage &blurImage, qreal radius, bool quality, bool alphaOnly, int transposed = 0 ); 729 | QT_END_NAMESPACE 730 | 731 | QPixmap pixmap = bg; 732 | QPainter painter( &pixmap ); 733 | // 填充半透明的背景颜色,避免太透 734 | { 735 | QColor bg_c(normal_bg); 736 | bg_c.setAlpha(normal_bg.alpha() * (100 - blur_bg_alpha) / 100); 737 | painter.fillRect(0, 0, pixmap.width(), pixmap.height(), bg_c); 738 | } 739 | QImage img = pixmap.toImage(); // img -blur-> painter(pixmap) 740 | qt_blurImage( &painter, img, radius, true, false ); 741 | // 裁剪掉边缘(模糊后会有黑边) 742 | int c = qMin(bg.width(), bg.height()); 743 | c = qMin(c/2, radius); 744 | bg_pixmap = pixmap.copy(c, c, pixmap.width()-c*2, pixmap.height()-c*2); 745 | } 746 | } 747 | 748 | // 有些重复显示的,需要再初始化一遍 749 | hidden_by_another = false; 750 | using_keyboard = false; 751 | closed_by_clicked = false; 752 | 753 | // 显示动画 754 | QWidget::show(); 755 | setFocus(); 756 | startAnimationOnShowed(); 757 | } 758 | 759 | /** 760 | * 逐级隐藏菜单,并带有选中项动画 761 | */ 762 | void FacileMenu::toHide(int focusIndex) 763 | { 764 | // 递归清理菜单(包括父菜单)焦点 765 | // 关闭的时候,会一层层的向上传递焦点 766 | // 虽然一般情况下问题不大,但在重命名(焦点不能丢失)时会出现严重问题 767 | FacileMenu* menu = this; 768 | do { 769 | menu->setFocusPolicy(Qt::NoFocus); 770 | menu = menu->parent_menu; 771 | } while (menu); 772 | this->clearFocus(); 773 | 774 | closed_by_clicked = true; 775 | startAnimationOnHidden(focusIndex); 776 | } 777 | 778 | void FacileMenu::toClose() 779 | { 780 | if (parent_menu) 781 | parent_menu->toClose(); 782 | else 783 | this->close(); 784 | } 785 | 786 | /** 787 | * 是因为点击了菜单项结束菜单 788 | * 还是因为其他原因,比如ESC关闭、鼠标点击其他位置呢 789 | */ 790 | bool FacileMenu::isClosedByClick() const 791 | { 792 | return closed_by_clicked; 793 | } 794 | 795 | /** 796 | * 菜单结束的时候调用 797 | * 例如多选,确认多选后可调用此项 798 | */ 799 | FacileMenu* FacileMenu::finished(FuncType func) 800 | { 801 | this->finished_func = new FuncType(func); 802 | return this; 803 | } 804 | 805 | /** 806 | * 添加可选菜单,快速添加多个单选项 807 | * @param texts 文字 808 | * @param states 选中状态 809 | * @param func 回调 810 | * @return 811 | */ 812 | FacileMenu *FacileMenu::addOptions(QList texts, QList states, FuncIntType clicked) 813 | { 814 | int si = qMin(texts.size(), states.size()); 815 | for (int i = 0; i < si; i++) 816 | { 817 | addAction(texts.at(i), [=]{ 818 | clicked(i); 819 | })->check(states.at(i)); 820 | } 821 | 822 | return this; 823 | } 824 | 825 | FacileMenu *FacileMenu::addOptions(QList texts, int select, FuncIntType clicked) 826 | { 827 | QListstates; 828 | for (int i = 0; i < texts.size(); i++) 829 | states << false; 830 | 831 | if (select >= 0 && select < states.size()) 832 | states[select] = true; 833 | 834 | return addOptions(texts, states, clicked); 835 | } 836 | 837 | /** 838 | * 用于单选 839 | * 取消除了某项之外全部选择 840 | */ 841 | FacileMenu *FacileMenu::singleCheck(FacileMenuItem *item) 842 | { 843 | uncheckAll(item); 844 | return this; 845 | } 846 | 847 | /** 848 | * 取消所有 checkable 的项的check 849 | * @param except 如果不为空,则设置这一项为true(相当于单选) 850 | * @param begin 开始取消选择的项,默认-1,从头开始 851 | * @param end 结束取消选择的项(不包括此项),默认-1,直到末尾 852 | */ 853 | FacileMenu *FacileMenu::uncheckAll(FacileMenuItem *except, int begin, int end) 854 | { 855 | if (begin < 0 || begin >= items.size()) 856 | begin = 0; 857 | if (end < 0 || end > items.size()) 858 | end = items.size(); 859 | for (int i = begin; i < end; i++) 860 | { 861 | auto item = items.at(i); 862 | if (!item->isCheckable()) 863 | continue; 864 | item->setChecked(false); 865 | } 866 | if (except) 867 | except->setChecked(true); 868 | return this; 869 | } 870 | 871 | /** 872 | * 返回选中的菜单 873 | */ 874 | QList FacileMenu::checkedItems() 875 | { 876 | QList checkeds; 877 | foreach (auto item, items) 878 | { 879 | if (item->isCheckable() && item->isChecked()) 880 | checkeds.append(item); 881 | } 882 | return checkeds; 883 | } 884 | 885 | /** 886 | * 返回选中项的索引 887 | * 该索引不包括自定义控件、布局等 888 | */ 889 | QList FacileMenu::checkedIndexes() 890 | { 891 | QList checkeds; 892 | for (int i = 0; i < items.size(); i++) 893 | { 894 | auto item = items.at(i); 895 | if (item->isCheckable() && item->isChecked()) 896 | checkeds.append(i); 897 | } 898 | return checkeds; 899 | } 900 | 901 | /** 902 | * 返回选中项中的text列表 903 | */ 904 | QStringList FacileMenu::checkedItemTexts() 905 | { 906 | QStringList texts; 907 | foreach (auto item, items) 908 | { 909 | if (item->isCheckable() && item->isChecked()) 910 | texts.append(item->getText()); 911 | } 912 | return texts; 913 | } 914 | 915 | /** 916 | * 返回选中项的自定义data列表 917 | * 如果没设置的话,会是无效的QVariant 918 | */ 919 | QList FacileMenu::checkedItemDatas() 920 | { 921 | QList datas; 922 | foreach (auto item, items) 923 | { 924 | if (item->isCheckable() && item->isChecked()) 925 | datas.append(item->getData()); 926 | } 927 | return datas; 928 | } 929 | 930 | /** 931 | * 一键设置已有菜单项为单选项 932 | * 注意,一定要在添加后设置,否则对后添加的项无效 933 | */ 934 | FacileMenu *FacileMenu::setSingleCheck(FuncCheckType clicked) 935 | { 936 | for (int i = 0; i < items.size(); i++) 937 | { 938 | auto item = items.at(i); 939 | if (!item->isCheckable()) 940 | item->setCheckable(true); 941 | 942 | item->triggered([=]{ 943 | item->toggle(); 944 | if (clicked) 945 | clicked(i, item->isChecked()); 946 | }); 947 | } 948 | return this; 949 | } 950 | 951 | /** 952 | * 一键设置已经菜单项为多选项 953 | * 注意,一定要在添加后设置,否则对后添加的项无效 954 | */ 955 | FacileMenu *FacileMenu::setMultiCheck(FuncCheckType clicked) 956 | { 957 | for (int i = 0; i < items.size(); i++) 958 | { 959 | auto item = items.at(i); 960 | item->setCheckable(true)->linger(); // 多选,点了一下不能消失 961 | 962 | item->triggered([=]{ 963 | item->toggle(); 964 | if (clicked) 965 | clicked(i, item->isChecked()); 966 | }); 967 | } 968 | return this; 969 | } 970 | 971 | /** 972 | * 设置右边提示的区域内容 973 | * 一般是快捷键 974 | * 尽量在添加菜单项前设置 975 | */ 976 | FacileMenu *FacileMenu::setTipArea(int x) 977 | { 978 | addin_tip_area = x; 979 | return this; 980 | } 981 | 982 | /** 983 | * 设置右边提示的区域内容 984 | * 一般是用来放快捷键 985 | * 尽量在添加菜单项前设置 986 | * @param tip 内容是什么不重要,只要等同于需要容纳的最长字符串即可(例如"ctrl+shit+alt+s") 987 | */ 988 | FacileMenu *FacileMenu::setTipArea(QString longestTip) 989 | { 990 | QFontMetrics fm(this->font()); 991 | addin_tip_area = fm.horizontalAdvance(longestTip + "Ctrl"); 992 | // 修改现有的 993 | foreach (auto item, items) 994 | item->setPaddings(item_padding, addin_tip_area > 0 ? tip_area_spacing + addin_tip_area : item_padding, item_padding, item_padding); 995 | return this; 996 | } 997 | 998 | /** 999 | * 设置是否默认分割同一行的菜单项 1000 | * 如果关闭,可手动使用 split() 或 addVSeparator() 分割 1001 | */ 1002 | FacileMenu *FacileMenu::setSplitInRow(bool split) 1003 | { 1004 | split_in_row = split; 1005 | return this; 1006 | } 1007 | 1008 | FacileMenu *FacileMenu::setBorderRadius(int r) 1009 | { 1010 | border_radius = r; 1011 | foreach (auto item, items) 1012 | item->subMenu() && item->subMenu()->setBorderRadius(r); 1013 | return this; 1014 | } 1015 | 1016 | FacileMenu *FacileMenu::setAppearAnimation(bool en) 1017 | { 1018 | this->enable_appear_animation = en; 1019 | foreach (auto item, items) 1020 | item->subMenu() && item->subMenu()->setAppearAnimation(en); 1021 | return this; 1022 | } 1023 | 1024 | FacileMenu *FacileMenu::setDisappearAnimation(bool en) 1025 | { 1026 | this->enable_disappear_animation = en; 1027 | foreach (auto item, items) 1028 | item->subMenu() && item->subMenu()->setDisappearAnimation(en); 1029 | return this; 1030 | } 1031 | 1032 | FacileMenu *FacileMenu::setSubMenuShowOnCursor(bool en) 1033 | { 1034 | this->sub_menu_show_on_cursor = en; 1035 | foreach (auto item, items) 1036 | item->subMenu() && item->subMenu()->setSubMenuShowOnCursor(en); 1037 | return this; 1038 | } 1039 | 1040 | void FacileMenu::itemMouseEntered(FacileMenuItem *item) 1041 | { 1042 | if (_showing_animation) 1043 | return ; 1044 | int index = items.indexOf(item); 1045 | if (using_keyboard && current_index > -1 && current_index < items.size() && current_index != index) // 屏蔽键盘操作 1046 | items.at(current_index)->discardHoverPress(true); 1047 | current_index = index; 1048 | 1049 | if (current_sub_menu) // 进入这个action,其他action展开的子菜单隐藏起来 1050 | { 1051 | current_sub_menu->hidden_by_another = true; 1052 | current_sub_menu->hide(); 1053 | current_sub_menu = nullptr; 1054 | } 1055 | } 1056 | 1057 | /** 1058 | * 所有的创建菜单项的方法 1059 | */ 1060 | FacileMenuItem *FacileMenu::createMenuItem(QIcon icon, QString text) 1061 | { 1062 | auto key = getShortcutByText(text); 1063 | text.replace(QRegExp("&([\\w\\d])\\b"), "\\1"); 1064 | FacileMenuItem* item; 1065 | if (!align_mid_if_alone) 1066 | { 1067 | item = new FacileMenuItem(icon, text, this); 1068 | } 1069 | else // 如果只有一项,则居中对齐 1070 | { 1071 | if (icon.isNull()) 1072 | item = new FacileMenuItem(text, this); 1073 | else if (text.isEmpty()) 1074 | item = new FacileMenuItem(icon, this); 1075 | else 1076 | item = new FacileMenuItem(icon, text, this); 1077 | } 1078 | item->setKey(key); 1079 | 1080 | setActionButton(item, adding_horizone); 1081 | 1082 | if (adding_horizone && row_hlayouts.size()) // 如果是正在添加横向按钮 1083 | { 1084 | if (split_in_row && row_hlayouts.last()->count() > 0) 1085 | addVSeparator(); // 添加竖向分割线 1086 | row_hlayouts.last()->addWidget(item); 1087 | } 1088 | else 1089 | { 1090 | // 超过了屏幕高度,需要添加另一列 1091 | if (main_vlayout->sizeHint().height() + item->sizeHint().height() > window_height) 1092 | createNextColumn(); 1093 | main_vlayout->addWidget(item); 1094 | } 1095 | items.append(item); 1096 | last_added_item = item; 1097 | 1098 | return item; 1099 | } 1100 | 1101 | Qt::Key FacileMenu::getShortcutByText(QString text) const 1102 | { 1103 | Qt::Key key = Qt::Key_Exit; 1104 | QRegularExpression re("&([\\d\\w])"); 1105 | auto match = re.match(text); 1106 | if (match.hasMatch()) 1107 | { 1108 | const QChar ch = match.capturedTexts().at(1).at(0); // 快捷键字符串 1109 | if (ch >= '0' && ch <= '9') 1110 | key = (Qt::Key)(Qt::Key_0 + (ch.toLatin1() - '0')); 1111 | else if (ch >= 'a' && ch <= 'z') 1112 | key = (Qt::Key)(Qt::Key_A + (ch.toUpper().toLatin1() - 'A')); 1113 | else if (ch >= 'A' && ch <= 'Z') 1114 | key = (Qt::Key)(Qt::Key_A + (ch.toUpper().toLatin1() - 'A')); 1115 | } 1116 | return key; 1117 | } 1118 | 1119 | /** 1120 | * 统一设置Action按钮(尺寸、颜色等) 1121 | * @param btn 按钮 1122 | * @param isChip 是否是小按钮(一行多个,右边不用空白) 1123 | */ 1124 | void FacileMenu::setActionButton(InteractiveButtonBase *btn, bool isChip) 1125 | { 1126 | // 设置尺寸 1127 | if (isChip) 1128 | { 1129 | btn->setPaddings(item_padding); 1130 | } 1131 | else 1132 | { 1133 | btn->setPaddings(item_padding, addin_tip_area > 0 ? tip_area_spacing + addin_tip_area : item_padding, item_padding, item_padding); 1134 | } 1135 | 1136 | // 设置颜色 1137 | btn->setNormalColor(Qt::transparent); 1138 | btn->setHoverColor(hover_bg); 1139 | btn->setPressColor(press_bg); 1140 | btn->setTextColor(text_fg); 1141 | 1142 | QFont font(btn->font()); 1143 | font.setWeight(QFont::Medium); 1144 | btn->setFont(font); 1145 | } 1146 | 1147 | void FacileMenu::showSubMenu(FacileMenuItem *item) 1148 | { 1149 | if (current_sub_menu) 1150 | { 1151 | if (item->subMenu() == current_sub_menu && !current_sub_menu->isHidden()) // 当前显示的就是这个子菜单 1152 | return ; 1153 | current_sub_menu->hidden_by_another = true; // 不隐藏父控件 1154 | current_sub_menu->hide(); 1155 | } 1156 | 1157 | if (item->subMenu()->items.count() == 0) // 没有菜单项,不显示 1158 | return ; 1159 | 1160 | current_sub_menu = item->subMenu(); 1161 | QPoint pos(-1, -1); 1162 | QRect avai = window_rect; 1163 | if (using_keyboard) // 键盘模式,不是跟随鼠标位置来的 1164 | { 1165 | // 键盘模式,相对于点击项的右边 1166 | QPoint tl = mapToGlobal(item->pos()); 1167 | if (tl.x() + item->width() + current_sub_menu->width() < avai.width()) 1168 | pos.setX(tl.x() + item->width()); 1169 | else 1170 | pos.setX(tl.x() - current_sub_menu->width()); 1171 | if (tl.y() + current_sub_menu->height() < avai.height()) 1172 | pos.setY(tl.y()); 1173 | else 1174 | pos.setY(tl.y() - current_sub_menu->height()); 1175 | } 1176 | if (sub_menu_show_on_cursor) 1177 | current_sub_menu->exec(pos); 1178 | else 1179 | { 1180 | auto geom = item->mapToGlobal(QPoint(0, 0)); 1181 | auto rect = QRect(geom, item->size()); 1182 | current_sub_menu->exec(rect, false, rect.topRight()); 1183 | } 1184 | current_sub_menu->setKeyBoardUsed(using_keyboard); 1185 | } 1186 | 1187 | /** 1188 | * 根据 mouseMove 事件,判断鼠标的位置 1189 | * 如果是在父菜单另一个item,则隐藏子菜单(接着触发关闭信号) 1190 | * 如果是在父菜单的父菜单,则关闭子菜单后,父菜单也依次关闭 1191 | * @param pos 鼠标位置(全局) 1192 | * @param child 子菜单 1193 | * @return 是否在父或递归父菜单的 geometry 中 1194 | */ 1195 | bool FacileMenu::isCursorInArea(QPoint pos, FacileMenu *child) 1196 | { 1197 | // 不在这范围内 1198 | if (!geometry().contains(pos)) 1199 | { 1200 | // 在自己的父菜单那里 1201 | if (isSubMenu() && parent_menu->isCursorInArea(pos, this)) // 如果这也是子菜单(已展开),则递归遍历父菜单 1202 | { 1203 | hidden_by_another = true; 1204 | QTimer::singleShot(0, this, [=]{ 1205 | close(); // 把自己也隐藏了 1206 | }); 1207 | return true; 1208 | } 1209 | // 在菜单栏那里 1210 | int rst = -1; 1211 | if (menu_bar && (rst = menu_bar->isCursorInArea(pos)) > -1) 1212 | { 1213 | menu_bar->triggerIfNot(rst, this); 1214 | return true; 1215 | } 1216 | return false; 1217 | } 1218 | // 如果是正展开的这个子项按钮 1219 | if (current_index > -1 && child && items.at(current_index)->subMenu() == child && items.at(current_index)->geometry().contains(mapFromGlobal(pos))) 1220 | return false; 1221 | // 在这个菜单内的其他按钮 1222 | return true; 1223 | } 1224 | 1225 | void FacileMenu::setKeyBoardUsed(bool use) 1226 | { 1227 | using_keyboard = use; 1228 | if (use && current_index == -1 && items.size()) // 还没有选中 1229 | { 1230 | items.at(current_index = 0)->simulateHover(); // 预先选中第一项 1231 | } 1232 | } 1233 | 1234 | bool FacileMenu::isSubMenu() const 1235 | { 1236 | return parent_menu != nullptr; 1237 | } 1238 | 1239 | /** 1240 | * 菜单出现动画 1241 | * 从光标位置依次出现 1242 | */ 1243 | void FacileMenu::startAnimationOnShowed() 1244 | { 1245 | if (!enable_appear_animation) 1246 | return ; 1247 | 1248 | main_vlayout->setEnabled(false); 1249 | _showing_animation = true; 1250 | QEasingCurve curve = easing_curve; 1251 | int duration = 300; 1252 | if (items.size() <= 1) 1253 | { 1254 | duration = 200; 1255 | } 1256 | 1257 | // 从上往下的动画 1258 | QPoint start_pos = mapFromGlobal(QCursor::pos()); 1259 | if (start_pos.x() < 0) 1260 | start_pos.setX(0); 1261 | else if (start_pos.x() > width()) 1262 | start_pos.setX(width()); 1263 | if (start_pos.y() < 0) 1264 | start_pos.setY(0); 1265 | else if (start_pos.y() > height()) 1266 | start_pos.setY(height()); 1267 | if (items.size() >= 1) 1268 | { 1269 | if (start_pos.y() == 0) // 最顶上 1270 | start_pos.setY(-items.at(0)->height()); 1271 | if (start_pos.x() == width()) // 最右边 1272 | start_pos.setX(width()/2); 1273 | } 1274 | for (int i = 0; i < items.size(); i++) 1275 | { 1276 | InteractiveButtonBase* btn = items.at(i); 1277 | btn->setBlockHover(true); 1278 | QPropertyAnimation* ani = new QPropertyAnimation(btn, "pos"); 1279 | ani->setStartValue(start_pos); 1280 | ani->setEndValue(btn->pos()); 1281 | ani->setEasingCurve(curve); 1282 | ani->setDuration(duration); 1283 | connect(ani, SIGNAL(finished()), ani, SLOT(deleteLater())); 1284 | connect(ani, &QPropertyAnimation::finished, btn, [=]{ 1285 | btn->setBlockHover(false); 1286 | }); 1287 | ani->start(); 1288 | } 1289 | for (int i = 0; i < other_widgets.size(); i++) 1290 | { 1291 | QWidget* btn = other_widgets.at(i); 1292 | QPropertyAnimation* ani = new QPropertyAnimation(btn, "pos"); 1293 | ani->setStartValue(start_pos); 1294 | ani->setEndValue(btn->pos()); 1295 | ani->setEasingCurve(curve); 1296 | ani->setDuration(duration); 1297 | connect(ani, SIGNAL(finished()), ani, SLOT(deleteLater())); 1298 | ani->start(); 1299 | } 1300 | 1301 | // 分割线动画 1302 | foreach (auto item, h_separators + v_separators) 1303 | item->hide(); 1304 | QTimer::singleShot(duration, this, [=]{ 1305 | for (int i = 0; i < h_separators.size(); i++) 1306 | { 1307 | InteractiveButtonBase* btn = h_separators.at(i); 1308 | btn->show(); 1309 | btn->setMinimumSize(0, 0); 1310 | QPropertyAnimation* ani = new QPropertyAnimation(btn, "geometry"); 1311 | ani->setStartValue(QRect(btn->geometry().center(), QSize(1,1))); 1312 | ani->setEndValue(btn->geometry()); 1313 | ani->setEasingCurve(QEasingCurve::OutQuad); 1314 | ani->setDuration(duration); 1315 | connect(ani, SIGNAL(finished()), ani, SLOT(deleteLater())); 1316 | ani->start(); 1317 | } 1318 | for (int i = 0; i < v_separators.size(); i++) 1319 | { 1320 | InteractiveButtonBase* btn = v_separators.at(i); 1321 | btn->show(); 1322 | btn->setMinimumSize(0, 0); 1323 | QPropertyAnimation* ani = new QPropertyAnimation(btn, "geometry"); 1324 | ani->setStartValue(QRect(btn->geometry().center(), QSize(1,1))); 1325 | ani->setEndValue(btn->geometry()); 1326 | ani->setEasingCurve(QEasingCurve::OutQuad); 1327 | ani->setDuration(duration); 1328 | connect(ani, SIGNAL(finished()), ani, SLOT(deleteLater())); 1329 | ani->start(); 1330 | } 1331 | }); 1332 | 1333 | QTimer::singleShot(duration, this, [=]{ 1334 | main_vlayout->setEnabled(true); 1335 | _showing_animation = false; 1336 | }); 1337 | } 1338 | 1339 | /** 1340 | * 关闭前显示隐藏动画 1341 | * @param focusIndex 聚焦的item,如果不存在则为-1 1342 | */ 1343 | void FacileMenu::startAnimationOnHidden(int focusIndex) 1344 | { 1345 | if (!enable_disappear_animation) 1346 | { 1347 | if (focusIndex > -1) 1348 | { 1349 | // 等待点击动画结束 1350 | QTimer::singleShot(100, [=]{ 1351 | close(); 1352 | }); 1353 | } 1354 | else 1355 | { 1356 | close(); 1357 | } 1358 | return ; 1359 | } 1360 | 1361 | _showing_animation = true; 1362 | // 控件移动动画 1363 | main_vlayout->setEnabled(false); 1364 | int dur_min =100, dur_max = 200; 1365 | int up_flow_count = focusIndex > -1 ? qMax(focusIndex, items.size()-focusIndex-1) : -1; 1366 | int up_end = items.size() ? -items.at(0)->height() : 0; 1367 | int flow_end = height(); 1368 | int focus_top = focusIndex > -1 ? items.at(focusIndex)->pos().y() : 0; 1369 | for (int i = 0; i < items.size(); i++) 1370 | { 1371 | InteractiveButtonBase* btn = items.at(i); 1372 | // QPoint pos = btn->pos(); 1373 | btn->setBlockHover(true); 1374 | QPropertyAnimation* ani = new QPropertyAnimation(btn, "pos"); 1375 | ani->setStartValue(btn->pos()); 1376 | ani->setEasingCurve(QEasingCurve::OutCubic); 1377 | if (focusIndex > -1) 1378 | { 1379 | if (i < focusIndex) // 上面的项 1380 | { 1381 | ani->setEndValue(QPoint(0, up_end - (focus_top - btn->pos().y()) / 8)); 1382 | ani->setDuration(dur_max - qAbs(focusIndex-i)*(dur_max-dur_min)/up_flow_count); 1383 | } 1384 | else if (i == focusIndex) // 中间的项 1385 | { 1386 | ani->setEndValue(btn->pos()); 1387 | ani->setDuration(dur_max); 1388 | } 1389 | else // 下面的项 1390 | { 1391 | ani->setEndValue(QPoint(0, flow_end + (btn->pos().y()-focus_top) / 8)); 1392 | ani->setDuration(dur_max - qAbs(i-focusIndex)*(dur_max-dur_min)/up_flow_count); 1393 | } 1394 | } 1395 | else 1396 | { 1397 | ani->setEndValue(QPoint(0, up_end)); 1398 | ani->setDuration(dur_max); 1399 | } 1400 | connect(ani, SIGNAL(finished()), ani, SLOT(deleteLater())); 1401 | connect(ani, &QPropertyAnimation::finished, btn, [=]{ 1402 | btn->setBlockHover(false); 1403 | // btn->move(pos); 1404 | }); 1405 | ani->start(); 1406 | } 1407 | 1408 | // 第三方控件动画 1409 | for (int i = 0; i < other_widgets.size(); i++) 1410 | { 1411 | QWidget* btn = other_widgets.at(i); 1412 | QPoint pos = btn->pos(); 1413 | QPropertyAnimation* ani = new QPropertyAnimation(btn, "pos"); 1414 | ani->setStartValue(btn->pos()); 1415 | ani->setEasingCurve(QEasingCurve::OutCubic); 1416 | if (focusIndex > -1) 1417 | { 1418 | if (i < focusIndex) // 上面的项 1419 | { 1420 | ani->setEndValue(QPoint(0, up_end - (focus_top - btn->pos().y()) / 8)); 1421 | ani->setDuration(dur_max - qAbs(focusIndex-i)*(dur_max-dur_min)/up_flow_count); 1422 | } 1423 | else if (i == focusIndex) // 中间的项 1424 | { 1425 | ani->setEndValue(btn->pos()); 1426 | ani->setDuration(dur_max); 1427 | } 1428 | else // 下面的项 1429 | { 1430 | ani->setEndValue(QPoint(0, flow_end + (btn->pos().y()-focus_top) / 8)); 1431 | ani->setDuration(dur_max - qAbs(i-focusIndex)*(dur_max-dur_min)/up_flow_count); 1432 | } 1433 | } 1434 | else 1435 | { 1436 | ani->setEndValue(QPoint(0, up_end)); 1437 | ani->setDuration(dur_max); 1438 | } 1439 | connect(ani, SIGNAL(finished()), ani, SLOT(deleteLater())); 1440 | // connect(ani, &QPropertyAnimation::finished, btn, [=]{ 1441 | // btn->move(pos); 1442 | // }); 1443 | ani->start(); 1444 | } 1445 | 1446 | // 变淡动画(针对Popup,一切透明无效) 1447 | /*QGraphicsOpacityEffect* effect = new QGraphicsOpacityEffect(this); 1448 | effect->setOpacity(1); 1449 | setGraphicsEffect(effect); 1450 | QPropertyAnimation* opa_ani = new QPropertyAnimation(effect, "opacity"); 1451 | opa_ani->setDuration(dur_max * 0.8); 1452 | opa_ani->setStartValue(1); 1453 | opa_ani->setEndValue(0); 1454 | connect(opa_ani, &QPropertyAnimation::finished, this, [=]{ 1455 | opa_ani->deleteLater(); 1456 | effect->deleteLater(); 1457 | }); 1458 | opa_ani->start();*/ 1459 | 1460 | // 真正关闭 1461 | QTimer::singleShot(dur_max, this, [=]{ 1462 | _showing_animation = false; 1463 | // 挨个还原之前的位置(不知道为什么main_vlayout不能恢复了) 1464 | main_vlayout->setEnabled(true); 1465 | main_vlayout->activate(); // 恢复原来的位置 1466 | 1467 | close(); 1468 | }); 1469 | } 1470 | 1471 | void FacileMenu::showEvent(QShowEvent *event) 1472 | { 1473 | QWidget::showEvent(event); 1474 | 1475 | main_vlayout->setEnabled(true); 1476 | main_vlayout->invalidate(); // 不清除缓存的话 activate 会false 1477 | main_vlayout->activate(); 1478 | } 1479 | 1480 | void FacileMenu::hideEvent(QHideEvent *event) 1481 | { 1482 | emit signalHidden(); 1483 | this->close(); // 子菜单关闭,不会导致自己关闭,需要手动close 1484 | return QWidget::hideEvent(event); 1485 | } 1486 | 1487 | void FacileMenu::mouseMoveEvent(QMouseEvent *event) 1488 | { 1489 | QWidget::mouseMoveEvent(event); 1490 | 1491 | QPoint pos = QCursor::pos(); 1492 | int rst = -1; 1493 | if ((_showing_animation && !menu_bar) || isCursorInArea(pos)) // 正在出现或在自己的区域内,不管 1494 | ; 1495 | else if (menu_bar && (rst = menu_bar->isCursorInArea(pos)) > -1) // 在菜单栏 1496 | { 1497 | menu_bar->triggerIfNot(rst, this); 1498 | } 1499 | else if (parent_menu && parent_menu->isCursorInArea(pos, this)) // 在父类,自己隐藏 1500 | { 1501 | this->hide(); 1502 | parent_menu->setFocus(); 1503 | } 1504 | 1505 | if (using_keyboard) 1506 | { 1507 | using_keyboard = false; 1508 | if (current_index >= 0 && current_index <= items.size()) 1509 | items.at(current_index)->discardHoverPress(false); 1510 | } 1511 | } 1512 | 1513 | void FacileMenu::keyPressEvent(QKeyEvent *event) 1514 | { 1515 | auto key = event->key(); 1516 | foreach (auto item, items) 1517 | { 1518 | if (item->isKey((Qt::Key)key)) 1519 | { 1520 | _showing_animation = false; // 在showing的时候,点击是无效的,所以要关掉 1521 | item->simulateStatePress(); // 确定是这个action的快捷键 1522 | return ; 1523 | } 1524 | } 1525 | 1526 | // 菜单按键响应 1527 | if (event->modifiers() != Qt::NoModifier || items.size() == 0) 1528 | return QWidget::keyPressEvent(event); 1529 | 1530 | switch (key) { 1531 | case Qt::Key_Up: 1532 | { 1533 | if (current_index < 0 || current_index >= items.size()) 1534 | current_index = items.size()-1; 1535 | else 1536 | { 1537 | items.at(current_index--)->discardHoverPress(true); 1538 | if (current_index < 0) 1539 | current_index = items.size()-1; 1540 | } 1541 | auto focusToHoriFirst = [&]{ 1542 | // 如果是横向按钮,理应聚焦到第一项;如果第一项不可用,依次延后 1543 | if (current_index > 0 && items.at(current_index-1)->pos().y() == items.at(current_index)->pos().y()) 1544 | { 1545 | int last_index = current_index; 1546 | // 获取当前行的第一项 1547 | int y = items.at(current_index)->pos().y(); 1548 | while (current_index > 0 && items.at(current_index-1)->pos().y() == y) 1549 | current_index--; 1550 | int first_index = current_index; 1551 | // 移动到第一项可用的项 1552 | while (current_index < last_index && !items.at(current_index)->isEnabled()) 1553 | current_index++; 1554 | // 如果这一行全部不可用,强制回到第一项 1555 | if (current_index == last_index) 1556 | current_index = first_index; 1557 | } 1558 | }; 1559 | focusToHoriFirst(); 1560 | // 判断 item 是否被禁用 1561 | if (!items.at(current_index)->isEnabled()) 1562 | { 1563 | int old_index = current_index; 1564 | do { 1565 | current_index--; 1566 | if (current_index < 0) 1567 | current_index = items.size()-1; 1568 | else 1569 | focusToHoriFirst(); 1570 | } while (current_index != old_index && !items.at(current_index)->isEnabled()); 1571 | if (current_index == old_index) // 如果绕了一圈都不能用,取消 1572 | return ; 1573 | } 1574 | // 找到真正的上一项 1575 | items.at(current_index)->simulateHover(); 1576 | using_keyboard = true; 1577 | return ; 1578 | } 1579 | case Qt::Key_Down: 1580 | { 1581 | if (current_index < 0 || current_index >= items.size()) 1582 | current_index = 0; 1583 | else 1584 | { 1585 | items.at(current_index++)->discardHoverPress(true); 1586 | if (current_index >= items.size()) 1587 | current_index = 0; 1588 | } 1589 | auto focusIgnoreHorizoneRest = [&]{ 1590 | // 跳过同一行后面所有(至少要先聚焦在这一行的第二项) 1591 | if (current_index > 0 && current_index < items.size()-1 && items.at(current_index-1)->pos().y() == items.at(current_index)->pos().y()) 1592 | { 1593 | int y = items.at(current_index)->pos().y(); 1594 | // 先判断前面是不是有可以点击的 1595 | int temp_index = current_index; 1596 | bool is_line_second = false; 1597 | while (--temp_index > 0 && items.at(temp_index)->pos().y() == y) 1598 | { 1599 | if (items.at(temp_index)->isEnabled()) 1600 | { 1601 | is_line_second = true; 1602 | break; 1603 | } 1604 | } 1605 | if (!is_line_second) // 这是这一行的第一项,可以聚焦 1606 | return ; 1607 | // 跳过这一行后面所有的按钮 1608 | while (current_index < items.size()-1 && items.at(current_index+1)->pos().y() == y) 1609 | current_index++; 1610 | current_index++; 1611 | if (current_index >= items.size()) 1612 | current_index = 0; 1613 | } 1614 | }; 1615 | focusIgnoreHorizoneRest(); 1616 | // 判断 item 是否被禁用 1617 | if (!items.at(current_index)->isEnabled()) 1618 | { 1619 | int old_index = current_index; 1620 | do { 1621 | current_index++; 1622 | if (current_index >= items.size()) 1623 | current_index = 0; 1624 | else 1625 | focusIgnoreHorizoneRest(); 1626 | } while (current_index != old_index && !items.at(current_index)->isEnabled()); 1627 | if (current_index == old_index) // 如果绕了一圈都不能用,取消 1628 | return ; 1629 | } 1630 | // 找到真正的上一项 1631 | items.at(current_index)->simulateHover(); 1632 | using_keyboard = true; 1633 | return ; 1634 | } 1635 | case Qt::Key_Left: 1636 | // 移动到左边按钮 1637 | if (current_index > 0 && items.at(current_index-1)->pos().y() == items.at(current_index)->pos().y()) 1638 | { 1639 | items.at(current_index--)->discardHoverPress(true); 1640 | // 找到左边第一项能点击的按钮;如果没有,宁可移动到上几行 1641 | if (!items.at(current_index)->isEnabled()) 1642 | { 1643 | int y = items.at(current_index)->pos().y(); 1644 | int ori_index = current_index; 1645 | /*while (--current_index >= 0 && !items.at(current_index)->isEnabled()) ; 1646 | if (current_index == -1) // 前面没有能选的了 1647 | { 1648 | current_index = ori_index + 1; // 恢复到之前选择的那一项 1649 | return ; 1650 | }*/ 1651 | while (--current_index >= 0 && !items.at(current_index)->isEnabled() && items.at(current_index)->pos().y() == y) ; 1652 | if (current_index < 0 || items.at(current_index)->pos().y() != y) // 前面没有能选的了 1653 | current_index = ori_index + 1; // 恢复到之前选择的那一项 1654 | } 1655 | // 聚焦到这个能点的按钮 1656 | items.at(current_index)->simulateHover(); 1657 | using_keyboard = true; 1658 | } 1659 | // 退出子菜单 1660 | else if (isSubMenu()) 1661 | close(); 1662 | return ; 1663 | case Qt::Key_Right: 1664 | // 移动到右边按钮 1665 | if (current_index < items.size()-1 && items.at(current_index+1)->pos().y() == items.at(current_index)->pos().y()) 1666 | { 1667 | items.at(current_index++)->discardHoverPress(true); 1668 | // 找到右边第一项能点击的按钮;如果没有,宁可移动到下几行 1669 | if (!items.at(current_index)->isEnabled()) 1670 | { 1671 | int y = items.at(current_index)->pos().y(); 1672 | int ori_index = current_index; 1673 | /*while (++current_index < items.size() && !items.at(current_index)->isEnabled()) ; 1674 | if (current_index == items.size()) // 后面没有能选的了 1675 | { 1676 | current_index = ori_index - 1; // 恢复到之前选择的那一项 1677 | return ; 1678 | }*/ 1679 | while (++current_index < items.size() && !items.at(current_index)->isEnabled() && items.at(current_index)->pos().y() == y) ; 1680 | if (current_index == items.size() || items.at(current_index)->pos().y() != y) // 后面没有能选的了 1681 | current_index = ori_index - 1; // 恢复到之前选择的那一项 1682 | } 1683 | // 聚焦到这个能点的按钮 1684 | items.at(current_index)->simulateHover(); 1685 | using_keyboard = true; 1686 | } 1687 | // 展开子菜单 1688 | else if (current_index >= 0 && current_index < items.size() && items.at(current_index)->isSubMenu()) 1689 | { 1690 | showSubMenu(items.at(current_index)); 1691 | } 1692 | return ; 1693 | case Qt::Key_Home: 1694 | if (current_index >= 0 || current_index < items.size()) 1695 | items.at(current_index)->discardHoverPress(true); 1696 | // 聚焦到第一项能点的按钮 1697 | if (!items.at(current_index = 0)->isEnabled()) 1698 | { 1699 | while (++current_index < items.size() && !items.at(current_index)->isEnabled()); 1700 | if(current_index == items.size()) // 没有能点的(不太可能) 1701 | return ; 1702 | } 1703 | items.at(current_index)->simulateHover(); 1704 | using_keyboard = true; 1705 | return ; 1706 | case Qt::Key_End: 1707 | if (current_index >= 0 || current_index < items.size()) 1708 | items.at(current_index)->discardHoverPress(true); 1709 | // 聚焦到最后一项能点的按钮 1710 | if (!items.at(current_index = items.size()-1)->isEnabled()) 1711 | { 1712 | while (--current_index >= 0 && !items.at(current_index)->isEnabled()); 1713 | if(current_index == -1) // 没有能点的(不太可能) 1714 | return ; 1715 | } 1716 | items.at(current_index)->simulateHover(); 1717 | using_keyboard = true; 1718 | return ; 1719 | case Qt::Key_Enter: 1720 | case Qt::Key_Return: 1721 | case Qt::Key_Space: 1722 | if (current_index >= 0 || current_index < items.size()) 1723 | { 1724 | auto item = items.at(current_index); 1725 | item->simulateStatePress(!item->getState(), false); 1726 | } 1727 | return ; 1728 | } 1729 | 1730 | return QWidget::keyPressEvent(event); 1731 | } 1732 | 1733 | void FacileMenu::paintEvent(QPaintEvent *event) 1734 | { 1735 | if (!bg_pixmap.isNull()) 1736 | { 1737 | QPainter painter(this); 1738 | painter.drawPixmap(QRect(0,0,width(),height()), bg_pixmap); 1739 | return ; 1740 | } 1741 | QWidget::paintEvent(event); 1742 | } 1743 | 1744 | void FacileMenu::setColors(QColor normal, QColor hover, QColor press, QColor text) 1745 | { 1746 | normal_bg = normal; 1747 | hover_bg = hover; 1748 | press_bg = press; 1749 | text_fg = text; 1750 | } 1751 | -------------------------------------------------------------------------------- /facile_menu/facilemenu.h: -------------------------------------------------------------------------------- 1 | #ifndef FACILEMENU_H 2 | #define FACILEMENU_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 "facilemenuitem.h" 16 | #include "facilemenubarinterface.h" 17 | 18 | #define DEFAULT_MENU_BLUR_ALPHA 33 19 | 20 | #define newFacileMenu FacileMenu *menu = (new FacileMenu(this)) // 加上括号是为了可以直接设置属性 21 | 22 | typedef std::function const FuncCheckType; 23 | 24 | class FacileMenu : public QWidget 25 | { 26 | Q_OBJECT 27 | public: 28 | FacileMenu(QWidget *parent = nullptr); 29 | ~FacileMenu() override; 30 | 31 | FacileMenuItem* addAction(QIcon icon, QString text, FuncType clicked = nullptr); 32 | FacileMenuItem* addAction(QIcon icon, FuncType clicked = nullptr); 33 | FacileMenuItem* addAction(QString text, FuncType clicked = nullptr); 34 | FacileMenuItem* addAction(QAction* action, bool deleteWithMenu = false); 35 | template 36 | FacileMenuItem* addAction(QIcon icon, QString text, T *obj, void (T::*func)()); 37 | FacileMenu* addNumberedActions(QString pattern, int numberStart, int numberEnd, FuncItemType config = nullptr, FuncIntType clicked = nullptr, int step = 0); 38 | FacileMenu* addNumberedActions(QString pattern, int numberStart, int numberEnd, FuncItemIntType config, FuncIntType clicked = nullptr, int step = 0); 39 | FacileMenu* addActions(QList actions); 40 | 41 | FacileMenu* addRow(FuncType addActions = []{}); 42 | FacileMenu* beginRow(); 43 | FacileMenu* endRow(); 44 | QVBoxLayout* createNextColumn(); 45 | QBoxLayout* currentLayout() const; 46 | FacileMenu* addTitle(QString text, int split = 0); 47 | 48 | FacileMenu* addMenu(QIcon icon, QString text, FuncType clicked = nullptr); 49 | FacileMenu* addMenu(QString text, FuncType clicked = nullptr); 50 | FacileMenu* addMenu(QMenu* menu); 51 | FacileMenuItem* parentAction(); 52 | FacileMenuItem* lastAction(); 53 | FacileMenuItem* currentAction(); 54 | 55 | FacileMenu* addLayout(QLayout *layout, int stretch = 0); 56 | FacileMenu* addLayoutItem(QLayoutItem *item); 57 | FacileMenu* addSpacerItem(QSpacerItem *spacerItem); 58 | FacileMenu* addSpacing(int size); 59 | FacileMenu* addStretch(int stretch = 0); 60 | FacileMenu* addStrut(int size); 61 | FacileMenu* addWidget(QWidget *widget, int stretch = 0, Qt::Alignment alignment = Qt::Alignment()); 62 | FacileMenu* setSpacing(int spacing); 63 | FacileMenu* setStretchFactor(QWidget *widget, int stretch); 64 | FacileMenu* setStretchFactor(QLayout *layout, int stretch); 65 | 66 | FacileMenuItem* addSeparator(); 67 | FacileMenu* split(); 68 | FacileMenuItem* lastAddedItem(); 69 | bool hasFocus() const; 70 | 71 | int indexOf(FacileMenuItem* item); 72 | FacileMenuItem* at(int index); 73 | void setMenuBar(FacileMenuBarInterface* mb); 74 | 75 | void exec(QPoint pos = QPoint(-1, -1)); 76 | void exec(QRect expt, bool vertical = false, QPoint pos = QPoint(-1, -1)); 77 | void execute(); 78 | void toHide(int focusIndex = -1); 79 | void toClose(); 80 | bool isClosedByClick() const; 81 | FacileMenu* finished(FuncType func); 82 | 83 | FacileMenu* addOptions(QListtexts, QListstates, FuncIntType clicked); 84 | FacileMenu* addOptions(QListtexts, int select, FuncIntType clicked); 85 | FacileMenu* singleCheck(FacileMenuItem* item); 86 | FacileMenu* uncheckAll(FacileMenuItem* except = nullptr, int begin = -1, int end = -1); 87 | QList checkedItems(); 88 | QList checkedIndexes(); 89 | QStringList checkedItemTexts(); 90 | QList checkedItemDatas(); 91 | FacileMenu* setSingleCheck(FuncCheckType clicked = nullptr); 92 | FacileMenu* setMultiCheck(FuncCheckType clicked = nullptr); 93 | 94 | FacileMenu* setTipArea(int x = 48); 95 | FacileMenu* setTipArea(QString longestTip); 96 | FacileMenu* setSplitInRow(bool split = true); 97 | FacileMenu* setBorderRadius(int r); 98 | 99 | FacileMenu* setAppearAnimation(bool en); 100 | FacileMenu* setDisappearAnimation(bool en); 101 | FacileMenu* setSubMenuShowOnCursor(bool en); 102 | 103 | signals: 104 | void signalActionTriggered(FacileMenuItem* action); 105 | void signalHidden(); // 只是隐藏了自己 106 | 107 | private slots: 108 | void itemMouseEntered(FacileMenuItem* item); 109 | 110 | protected: 111 | FacileMenu(bool sub, QWidget* parent = nullptr); 112 | FacileMenuItem* createMenuItem(QIcon icon, QString text); 113 | Qt::Key getShortcutByText(QString text) const; 114 | void setActionButton(InteractiveButtonBase* btn, bool isChip = false); 115 | void showSubMenu(FacileMenuItem* item); 116 | bool isCursorInArea(QPoint pos, FacileMenu* child = nullptr); 117 | void setKeyBoardUsed(bool use = true); 118 | bool isSubMenu() const; 119 | FacileMenuItem* addVSeparator(); 120 | void startAnimationOnShowed(); 121 | void startAnimationOnHidden(int focusIndex); 122 | 123 | void showEvent(QShowEvent *event) override; 124 | void hideEvent(QHideEvent *event) override; 125 | void mouseMoveEvent(QMouseEvent *event) override; 126 | void keyPressEvent(QKeyEvent *event) override; 127 | void paintEvent(QPaintEvent *event) override; 128 | 129 | public: 130 | static void setColors(QColor normal, QColor hover, QColor press, QColor text); 131 | static QColor normal_bg; // 普通背景(用作全局是为了方便设置) 132 | static QColor hover_bg; // 悬浮背景 133 | static QColor press_bg; // 按下背景 134 | static QColor text_fg; // 字体/变色图标颜色 135 | static int blur_bg_alpha; // 背景图显示程度,0禁用,1~100为模糊透明度 136 | static QEasingCurve easing_curve; // 出现的动画曲线 137 | 138 | private: 139 | QList items; 140 | QList v_separators, h_separators; 141 | QList other_widgets; // 手动添加的widget 142 | QHBoxLayout* main_hlayout; 143 | QVBoxLayout* main_vlayout; 144 | QList row_hlayouts; 145 | QList import_actions; 146 | QPixmap bg_pixmap; 147 | 148 | FacileMenu* current_sub_menu = nullptr; // 当前打开(不一定显示)的子菜单 149 | FacileMenu* parent_menu = nullptr; // 父对象的菜单 150 | FacileMenuItem* last_added_item = nullptr; // 最后添加的item 151 | FacileMenuBarInterface* menu_bar = nullptr; // 菜单栏接口 152 | FuncType* finished_func = nullptr; // 析构前要执行的 153 | 154 | bool hidden_by_another = false; // 是否是被要显示的另一个子菜单替换了。若否,隐藏全部菜单 155 | const int item_padding = 8; // 每个item四周的空白 156 | const int tip_area_spacing = 8; // item正文和tip的间距 157 | bool adding_horizone = false; // 是否正在添加横向菜单 158 | bool align_mid_if_alone = false; // 是否居中对齐,如果只有icon或text 159 | bool linger_on_submenu_clicked = false; // 子菜单点击后,父菜单是否逐级隐藏(注意子菜单若有选择项需手动改变) 160 | bool _showing_animation = false; 161 | int current_index = -1; // 当前索引 162 | bool using_keyboard = false; // 是否正在使用键盘挑选菜单 163 | QRect window_rect; 164 | int window_height = 0; // 窗口高度,每次打开都更新一次 165 | QPoint _enter_later_pos = QPoint(-1, -1); // 避免连续两次触发 enterLater 事件 166 | bool closed_by_clicked = false; // 是否因为被单击了才隐藏,还是因为其他原因关闭 167 | 168 | // 可修改的配置属性 169 | int addin_tip_area = 48; // 右边用来显示提示文字的区域 170 | int border_radius = 5; // 圆角 171 | bool split_in_row = false; // 同一行是否默认添加分割线 172 | bool enable_appear_animation = true; 173 | bool enable_disappear_animation = true; 174 | bool sub_menu_show_on_cursor = true; // 子菜单跟随鼠标出现还是在主菜单边缘 175 | }; 176 | 177 | #endif // FACILEMENU_H 178 | -------------------------------------------------------------------------------- /facile_menu/facilemenuanimation.h: -------------------------------------------------------------------------------- 1 | #ifndef FACILEMENUANIMATION_H 2 | #define FACILEMENUANIMATION_H 3 | 4 | #include 5 | #include "interactivebuttonbase.h" 6 | 7 | class FacileBackgrounWidget : QWidget 8 | { 9 | 10 | }; 11 | 12 | class FacileSwitchWidget : public QWidget 13 | { 14 | Q_OBJECT 15 | Q_PROPERTY(double switch_ani READ getSwitchAni WRITE setSwitchAni) 16 | public: 17 | FacileSwitchWidget(QWidget* w1, QWidget* w2) : QWidget(nullptr) 18 | { 19 | // 设置属性 20 | setWindowFlags(Qt::Popup | Qt::FramelessWindowHint); 21 | 22 | // 设置圆角矩形 23 | int borderRadius = 0; 24 | if (borderRadius) 25 | { 26 | QPixmap pixmap(width(), height()); 27 | pixmap.fill(Qt::transparent); 28 | QPainter pix_ptr(&pixmap); 29 | pix_ptr.setRenderHint(QPainter::Antialiasing, true); 30 | QPainterPath path; 31 | path.addRoundedRect(0, 0, width(), height(), borderRadius, borderRadius); 32 | pix_ptr.fillPath(path, Qt::white); 33 | setMask(pixmap.mask()); 34 | } 35 | 36 | // 创建状态 37 | pixmap1 = QPixmap(w1->size()); 38 | w1->render(&pixmap1); 39 | pixmap2 = QPixmap(w2->size()); 40 | w2->render(&pixmap2); 41 | 42 | // 动画设置 43 | int duration = 150; 44 | 45 | // 绘制动画 46 | QPropertyAnimation* ani = new QPropertyAnimation(this, "switch_ani"); 47 | ani->setStartValue(0); 48 | ani->setEndValue(255); 49 | ani->setDuration(duration); 50 | ani->setEasingCurve(QEasingCurve::OutSine); 51 | ani->start(); 52 | connect(ani, &QPropertyAnimation::stateChanged, this, [=](QPropertyAnimation::State state) { 53 | if (state == QPropertyAnimation::State::Stopped) 54 | ani->deleteLater(); 55 | }); 56 | 57 | // 移动动画 58 | ani = new QPropertyAnimation(this, "geometry"); 59 | ani->setStartValue(w1->geometry()); 60 | ani->setEndValue(w2->geometry()); 61 | ani->setDuration(duration); 62 | ani->setEasingCurve(QEasingCurve::OutSine); 63 | connect(ani, SIGNAL(valueChanged(const QVariant &)), this, SLOT(update())); 64 | ani->start(); 65 | connect(ani, &QPropertyAnimation::stateChanged, this, [=](QPropertyAnimation::State state) { 66 | if (state == QPropertyAnimation::State::Stopped) 67 | { 68 | emit finished(); 69 | ani->deleteLater(); 70 | this->deleteLater(); 71 | } 72 | }); 73 | 74 | this->show(); 75 | } 76 | 77 | protected: 78 | void paintEvent(QPaintEvent *event) override 79 | { 80 | QWidget::paintEvent(event); 81 | 82 | QPainter painter(this); 83 | painter.setOpacity(qMax(0.0, 192 - switch_ani) / 192); 84 | painter.drawPixmap(rect(), pixmap1); 85 | painter.setOpacity(qMax(0.0, switch_ani-64) / 192); 86 | painter.drawPixmap(rect(), pixmap2); 87 | } 88 | 89 | double getSwitchAni() const 90 | { 91 | return switch_ani; 92 | } 93 | 94 | void setSwitchAni(double x) 95 | { 96 | this->switch_ani = x; 97 | } 98 | 99 | signals: 100 | void finished(); 101 | 102 | private: 103 | QPixmap pixmap1; 104 | QPixmap pixmap2; 105 | double alpha1 = 0; 106 | double alpha2 = 0; 107 | double switch_ani = 0; 108 | }; 109 | 110 | #endif // FACILEMENUANIMATION_H 111 | -------------------------------------------------------------------------------- /facile_menu/facilemenubar.cpp: -------------------------------------------------------------------------------- 1 | #include "facilemenubar.h" 2 | #include "facilemenuanimation.h" 3 | 4 | FacileMenuBar::FacileMenuBar(QWidget *parent) : QWidget(parent) 5 | { 6 | hlayout = new QHBoxLayout(this); 7 | hlayout->setAlignment(Qt::AlignLeft); 8 | hlayout->setSpacing(0); 9 | hlayout->setMargin(0); 10 | setLayout(hlayout); 11 | } 12 | 13 | /// 鼠标是否在这个区域内 14 | /// @param pos 绝对位置 15 | /// @return 所在的 MenuButton 索引,-1表示不在 16 | int FacileMenuBar::isCursorInArea(QPoint pos) const 17 | { 18 | QPoint mPos = mapFromGlobal(pos); 19 | if (!rect().contains(mPos)) 20 | return -1; 21 | for (int i = 0; i < buttons.size(); i++) 22 | { 23 | if (buttons.at(i)->geometry().contains(mPos)) 24 | return i; 25 | } 26 | return -1; 27 | } 28 | 29 | int FacileMenuBar::currentIndex() const 30 | { 31 | return _currentIndex; 32 | } 33 | 34 | /// 准备激活第index个菜单 35 | /// 如果不是参数menu,则激活该菜单 36 | bool FacileMenuBar::triggerIfNot(int index, void *menu) 37 | { 38 | // 非菜单栏中的按钮,隐藏全部 39 | FacileMenu* m = static_cast(menu); 40 | if (index < 0 || index >= buttons.size()) 41 | { 42 | if (m) 43 | m->hide(); 44 | return true; 45 | } 46 | 47 | // 切换按钮 48 | if (menus.at(index) == menu) 49 | { 50 | return false; 51 | } 52 | 53 | if (m && enableAnimation) 54 | { 55 | // 带动画的trigger 56 | switchTrigger(index, menus.indexOf(m)); 57 | } 58 | else 59 | { 60 | if (m) 61 | m->close(); 62 | trigger(index); 63 | } 64 | 65 | return true; 66 | } 67 | 68 | void FacileMenuBar::addMenu(QString name, FacileMenu *menu) 69 | { 70 | auto btn = createButton(name, menu); 71 | 72 | buttons.append(btn); 73 | menus.append(menu); 74 | hlayout->addWidget(btn); 75 | } 76 | 77 | void FacileMenuBar::insertMenu(int index, QString name, FacileMenu *menu) 78 | { 79 | auto btn = createButton(name, menu); 80 | 81 | buttons.insert(index, btn); 82 | menus.insert(index, menu); 83 | hlayout->insertWidget(index, btn); 84 | } 85 | 86 | void FacileMenuBar::deleteMenu(int index) 87 | { 88 | if (index < 0 || index >= buttons.size()) 89 | return ; 90 | buttons.takeAt(index)->deleteLater(); 91 | menus.takeAt(index)->deleteLater(); 92 | } 93 | 94 | int FacileMenuBar::count() const 95 | { 96 | return buttons.size(); 97 | } 98 | 99 | void FacileMenuBar::setAnimationEnabled(bool en) 100 | { 101 | this->enableAnimation = en; 102 | } 103 | 104 | /// 显示菜单 105 | void FacileMenuBar::trigger(int index) 106 | { 107 | if (index < 0 || index >= buttons.size()) 108 | return ; 109 | 110 | if (aniWidget) 111 | { 112 | aniWidget->deleteLater(); 113 | aniWidget = nullptr; 114 | } 115 | 116 | InteractiveButtonBase* btn = buttons.at(index); 117 | btn->setBgColor(FacileMenu::hover_bg); 118 | QRect rect(btn->mapToGlobal(QPoint(0, 0)), btn->size()); 119 | if (enableAnimation) 120 | menus.at(index)->setAppearAnimation(true); // 要重新设置一下,因为取消掉了 121 | menus.at(index)->exec(rect, true); 122 | _currentIndex = index; 123 | } 124 | 125 | void FacileMenuBar::switchTrigger(int index, int prevIndex) 126 | { 127 | if (index < 0 || index >= buttons.size()) 128 | return ; 129 | if (prevIndex < 0 || prevIndex >= buttons.size()) 130 | return trigger(index); 131 | 132 | InteractiveButtonBase* btn = buttons.at(index); 133 | QRect rect(btn->mapToGlobal(QPoint(0, 0)), btn->size()); 134 | 135 | FacileMenu* m = menus.at(index); 136 | m->setAppearAnimation(false); // 必须要关闭显示动画 137 | menus.at(prevIndex)->hide(); // 避免影响背景 138 | m->exec(rect, true); 139 | 140 | if (aniWidget) 141 | { 142 | aniWidget->deleteLater(); 143 | aniWidget = nullptr; 144 | } 145 | 146 | auto w = new FacileSwitchWidget(menus.at(prevIndex), menus.at(index)); 147 | aniWidget = w; 148 | QPoint pos = m->pos(); 149 | m->move(-10000 - m->width(), -10000 - m->height()); // 要保持显示,才能监控鼠标位置;因为可能立刻移开了 150 | btn->setBgColor(FacileMenu::hover_bg); // 上面的hide会触发关闭事件,取消颜色 151 | connect(w, &FacileSwitchWidget::finished, w, [=]{ 152 | m->move(pos); 153 | m->show(); 154 | aniWidget = nullptr; // 自动deleteLater 155 | }); 156 | _currentIndex = index; 157 | } 158 | 159 | InteractiveButtonBase *FacileMenuBar::createButton(QString name, FacileMenu *menu) 160 | { 161 | InteractiveButtonBase* btn = new InteractiveButtonBase(name, this); 162 | // TODO: name 识别并注册快捷键 163 | connect(btn, &InteractiveButtonBase::clicked, this, [=]{ 164 | int index = buttons.indexOf(btn); 165 | if (!menus.at(index)->isHidden()) 166 | { 167 | // 如果是正在显示状态 168 | // 但是理论上来说不可能,因为这种情况是点不到按钮的 169 | menus.at(index)->hide(); 170 | } 171 | else 172 | { 173 | // 点击触发菜单 174 | trigger(buttons.indexOf(btn)); 175 | } 176 | }); 177 | connect(menu, &FacileMenu::signalHidden, btn, [=]{ 178 | btn->setBgColor(Qt::transparent); 179 | _currentIndex = -1; 180 | }); 181 | 182 | btn->adjustMinimumSize(); 183 | btn->setRadius(5); 184 | btn->setFixedForePos(); 185 | 186 | menu->setMenuBar(this); 187 | menu->setAttribute(Qt::WA_DeleteOnClose, false); 188 | // menu->setAppearAnimation(false); // 设置显示动画 189 | menu->setDisappearAnimation(false); // 设置关闭动画 190 | menu->setSubMenuShowOnCursor(false); 191 | if (enableAnimation) 192 | menu->setBorderRadius(0); 193 | return btn; 194 | } 195 | -------------------------------------------------------------------------------- /facile_menu/facilemenubar.h: -------------------------------------------------------------------------------- 1 | #ifndef FACILEMENUBAR_H 2 | #define FACILEMENUBAR_H 3 | 4 | #include 5 | #include 6 | #include "facilemenubarinterface.h" 7 | #include "facilemenu.h" 8 | 9 | class FacileSwitchWidget; 10 | 11 | class FacileMenuBar : public QWidget, public FacileMenuBarInterface 12 | { 13 | Q_OBJECT 14 | public: 15 | explicit FacileMenuBar(QWidget *parent = nullptr); 16 | 17 | virtual int isCursorInArea(QPoint pos) const override; 18 | int currentIndex() const override; 19 | virtual bool triggerIfNot(int index, void*menu) override; 20 | 21 | void addMenu(QString name, FacileMenu* menu); 22 | void insertMenu(int index, QString name, FacileMenu* menu); 23 | void deleteMenu(int index); 24 | int count() const; 25 | void setAnimationEnabled(bool en); // 开启动画,目前会有些问题 26 | 27 | signals: 28 | void triggered(); 29 | 30 | public slots: 31 | virtual void trigger(int index) override; 32 | virtual void switchTrigger(int index, int prevIndex); 33 | 34 | private: 35 | InteractiveButtonBase* createButton(QString name, FacileMenu* menu); 36 | 37 | private: 38 | QList buttons; 39 | QList menus; 40 | QHBoxLayout* hlayout; 41 | 42 | int _currentIndex = -1; 43 | bool enableAnimation = false; 44 | FacileSwitchWidget* aniWidget = nullptr; 45 | }; 46 | 47 | #endif // FACILEMENUBAR_H 48 | -------------------------------------------------------------------------------- /facile_menu/facilemenubarinterface.h: -------------------------------------------------------------------------------- 1 | #ifndef FACILEMENUBARINTERFACE_H 2 | #define FACILEMENUBARINTERFACE_H 3 | 4 | #include 5 | 6 | class FacileMenuBarInterface 7 | { 8 | public: 9 | virtual void trigger(int) = 0; 10 | virtual bool triggerIfNot(int, void*) = 0; 11 | virtual int isCursorInArea(QPoint) const = 0; 12 | virtual int currentIndex() const = 0; 13 | }; 14 | 15 | #endif // FACILEMENUBARINTERFACE_H 16 | -------------------------------------------------------------------------------- /facile_menu/facilemenuitem.cpp: -------------------------------------------------------------------------------- 1 | #include "facilemenuitem.h" 2 | 3 | FacileMenuItem::FacileMenuItem(QWidget *parent) : InteractiveButtonBase(parent) 4 | { 5 | 6 | } 7 | 8 | FacileMenuItem::FacileMenuItem(QString t, QWidget *parent) : InteractiveButtonBase(t, parent) 9 | { 10 | 11 | } 12 | 13 | FacileMenuItem::FacileMenuItem(QIcon i, QWidget *parent) : InteractiveButtonBase(i, parent) 14 | { 15 | 16 | } 17 | 18 | FacileMenuItem::FacileMenuItem(QIcon i, QString t, QWidget *parent) : InteractiveButtonBase(i, t, parent) 19 | { 20 | 21 | } 22 | 23 | FacileMenuItem::FacileMenuItem(QPixmap p, QString t, QWidget *parent) : InteractiveButtonBase(p, t, parent) 24 | { 25 | 26 | } 27 | 28 | FacileMenuItem *FacileMenuItem::setEnabled(bool e) 29 | { 30 | InteractiveButtonBase::setEnabled(e); 31 | return this; 32 | } 33 | 34 | FacileMenuItem *FacileMenuItem::setCheckable(bool c) 35 | { 36 | checkable = c; 37 | if (c && model != IconText && InteractiveButtonBase::icon.isNull()) 38 | model = IconText; 39 | update(); 40 | return this; 41 | } 42 | 43 | bool FacileMenuItem::isCheckable() const 44 | { 45 | return checkable; 46 | } 47 | 48 | FacileMenuItem *FacileMenuItem::setChecked(bool c) 49 | { 50 | _state = c; 51 | if (InteractiveButtonBase::icon.isNull()) 52 | model = IconText; // 强制显示check空白部分 53 | setCheckable(true); 54 | return this; 55 | } 56 | 57 | bool FacileMenuItem::isChecked() 58 | { 59 | return getState(); 60 | } 61 | 62 | FacileMenuItem *FacileMenuItem::setKey(Qt::Key key) 63 | { 64 | this->key = key; 65 | return this; 66 | } 67 | 68 | bool FacileMenuItem::isKey(Qt::Key key) const 69 | { 70 | return key == this->key; 71 | } 72 | 73 | FacileMenuItem *FacileMenuItem::setSubMenu(FacileMenu *menu) 74 | { 75 | sub_menu = menu; 76 | return this; 77 | } 78 | 79 | bool FacileMenuItem::isSubMenu() const 80 | { 81 | return sub_menu != nullptr; 82 | } 83 | 84 | bool FacileMenuItem::isLinger() const 85 | { 86 | return trigger_linger; 87 | } 88 | 89 | FacileMenuItem *FacileMenuItem::setData(QVariant data) 90 | { 91 | this->data = data; 92 | return this; 93 | } 94 | 95 | QVariant FacileMenuItem::getData() 96 | { 97 | return data; 98 | } 99 | 100 | FacileMenuItem *FacileMenuItem::tip(QString sc) 101 | { 102 | shortcut_tip = sc; 103 | return this; 104 | } 105 | 106 | FacileMenuItem *FacileMenuItem::tip(bool exp, QString sc) 107 | { 108 | if (exp) 109 | tip(sc); 110 | return this; 111 | } 112 | 113 | FacileMenuItem *FacileMenuItem::tooltip(QString tt) 114 | { 115 | setToolTip(tt); 116 | return this; 117 | } 118 | 119 | FacileMenuItem *FacileMenuItem::tooltip(bool exp, QString tt) 120 | { 121 | if (exp) 122 | tooltip(tt); 123 | return this; 124 | } 125 | 126 | FacileMenuItem *FacileMenuItem::triggered(FuncType func) 127 | { 128 | connect(this, &InteractiveButtonBase::clicked, this, [=]{ 129 | func(); 130 | }); 131 | return this; 132 | } 133 | 134 | FacileMenuItem *FacileMenuItem::triggered(bool exp, FuncType func) 135 | { 136 | if (!exp) 137 | triggered(func); 138 | return this; 139 | } 140 | 141 | FacileMenuItem *FacileMenuItem::disable(bool exp) 142 | { 143 | if (exp) 144 | setDisabled(true); 145 | return this; 146 | } 147 | 148 | FacileMenuItem *FacileMenuItem::enable(bool exp) 149 | { 150 | if (exp) 151 | setEnabled(true); 152 | return this; 153 | } 154 | 155 | FacileMenuItem *FacileMenuItem::hide(bool exp) 156 | { 157 | if (exp) 158 | InteractiveButtonBase::hide(); 159 | return this; 160 | } 161 | 162 | /** 163 | * 默认就是show状态 164 | * 为了和show区分开 165 | */ 166 | FacileMenuItem *FacileMenuItem::visible(bool exp) 167 | { 168 | if (exp) 169 | InteractiveButtonBase::setVisible(true); 170 | return this; 171 | } 172 | 173 | FacileMenuItem *FacileMenuItem::check(bool exp) 174 | { 175 | setCheckable(true); 176 | if (exp) 177 | setChecked(true); 178 | else if (InteractiveButtonBase::icon.isNull()) 179 | model = IconText; // 强制显示check空白部分 180 | return this; 181 | } 182 | 183 | FacileMenuItem *FacileMenuItem::uncheck(bool exp) 184 | { 185 | setCheckable(true); 186 | if (exp) 187 | setChecked(false); 188 | return this; 189 | } 190 | 191 | /** 192 | * 切换状态 193 | * 如果选中了,则取消选中;反之亦然 194 | * (本来打算不只是选中状态,然而还没想到其他有什么能切换的) 195 | */ 196 | FacileMenuItem *FacileMenuItem::toggle(bool exp) 197 | { 198 | if (!exp) 199 | return this; 200 | if (isCheckable()) 201 | { 202 | setChecked(!isChecked()); 203 | } 204 | // 以后什么功能想到再加 205 | return this; 206 | } 207 | 208 | /** 209 | * 点击自动切换状态 210 | * 小心点,因为信号槽顺序的关系,若放在triggered后面,可能会出现相反的check 211 | * 建议只用于和顺序无关的自动切换 212 | * (还不知道怎么修改信号槽的调用顺序) 213 | */ 214 | FacileMenuItem *FacileMenuItem::autoToggle() 215 | { 216 | connect(this, &InteractiveButtonBase::clicked, this, [=]{ 217 | toggle(); 218 | }); 219 | return this; 220 | } 221 | 222 | FacileMenuItem *FacileMenuItem::text(bool exp, QString str) 223 | { 224 | if (exp) 225 | { 226 | // 去掉快捷键符号 227 | // 注意:这里设置文字不会改变原来的快捷键! 228 | setText(str.replace(QRegExp("&([\\w\\d])\\b"), "\\1")); 229 | // 调整大小 230 | setFixedForeSize(); 231 | } 232 | return this; 233 | } 234 | 235 | /** 236 | * 设置字符串,成立时 tru,不成立时 fal 237 | * 注意:这里是直接设置完整的文字,不会去掉快捷键&符号 238 | */ 239 | FacileMenuItem *FacileMenuItem::text(bool exp, QString tru, QString fal) 240 | { 241 | if (exp) 242 | setText(tru); 243 | else 244 | setText(fal); 245 | return this; 246 | } 247 | 248 | FacileMenuItem *FacileMenuItem::fgColor(QColor color) 249 | { 250 | setTextColor(color); 251 | return this; 252 | } 253 | 254 | FacileMenuItem *FacileMenuItem::fgColor(bool exp, QColor color) 255 | { 256 | if (exp) 257 | return fgColor(color); 258 | return this; 259 | } 260 | 261 | FacileMenuItem *FacileMenuItem::bgColor(QColor color) 262 | { 263 | setBgColor(color); 264 | return this; 265 | } 266 | 267 | FacileMenuItem *FacileMenuItem::bgColor(bool exp, QColor color) 268 | { 269 | if (exp) 270 | bgColor(color); 271 | return this; 272 | } 273 | 274 | /** 275 | * 满足条件时,text添加前缀 276 | */ 277 | FacileMenuItem *FacileMenuItem::prefix(bool exp, QString pfix) 278 | { 279 | if (exp) 280 | prefix(pfix); 281 | return this; 282 | } 283 | 284 | /** 285 | * 满足条件时,text添加后缀 286 | * @param inLeftParenthesis 支持 text(xxx) 形式,会在左括号前添加后缀 287 | */ 288 | FacileMenuItem *FacileMenuItem::suffix(bool exp, QString sfix, bool inLeftParenthesis) 289 | { 290 | if (exp) 291 | { 292 | suffix(sfix, inLeftParenthesis); 293 | } 294 | return this; 295 | } 296 | 297 | FacileMenuItem *FacileMenuItem::prefix(QString pfix) 298 | { 299 | setText(pfix + getText()); 300 | return this; 301 | } 302 | 303 | FacileMenuItem *FacileMenuItem::suffix(QString sfix, bool inLeftParenthesis) 304 | { 305 | if (!inLeftParenthesis) 306 | { 307 | setText(getText() + sfix); 308 | } 309 | else 310 | { 311 | QString text = getText(); 312 | int index = -1; 313 | if ((index = text.lastIndexOf("(")) > -1) 314 | { 315 | while (index > 0 && text.mid(index-1, 1) == " ") 316 | index--; 317 | } 318 | if (index <= 0) // 没有左括号或者以空格开头,直接加到最后面 319 | { 320 | setText(getText() + sfix); 321 | } 322 | else 323 | { 324 | setText(text.left(index) + sfix + text.right(text.length()-index)); 325 | } 326 | } 327 | return this; 328 | } 329 | 330 | FacileMenuItem *FacileMenuItem::icon(bool exp, QIcon ico) 331 | { 332 | if (exp) 333 | setIcon(ico); 334 | return this; 335 | } 336 | 337 | FacileMenuItem *FacileMenuItem::borderR(int radius, QColor co) 338 | { 339 | setRadius(radius); 340 | if (co != Qt::transparent) 341 | setBorderColor(co); 342 | else 343 | setBorderColor(press_bg); 344 | return this; 345 | } 346 | 347 | FacileMenuItem *FacileMenuItem::linger() 348 | { 349 | trigger_linger = true; 350 | return this; 351 | } 352 | 353 | FacileMenuItem *FacileMenuItem::lingerText(QString textAfterClick) 354 | { 355 | this->linger(); 356 | connect(this, &FacileMenuItem::clicked, this, [=]{ 357 | setText(textAfterClick); 358 | }); 359 | return this; 360 | } 361 | 362 | /** 363 | * 绑定某一布尔类型的变量(只能全局变量) 364 | * 点击即切换值 365 | * 注意:因为是异步的,局部变量会导致崩溃! 366 | */ 367 | FacileMenuItem *FacileMenuItem::bind(bool &val) 368 | { 369 | connect(this, &InteractiveButtonBase::clicked, this, [&]{ 370 | val = !val; 371 | }); 372 | return this; 373 | } 374 | 375 | /** 376 | * 短期长按效果 377 | * 该操作不会影响其它任何交互效果 378 | * 即不会隐藏菜单,也不会解除单击信号 379 | */ 380 | FacileMenuItem *FacileMenuItem::longPress(FuncType func) 381 | { 382 | connect(this, &InteractiveButtonBase::signalMousePressLater, this, [=](QMouseEvent*){ 383 | func(); 384 | }); 385 | return this; 386 | } 387 | 388 | /** 389 | * 点击菜单后的新文本 390 | * 一般用于点击后不隐藏 或者 重新显示的菜单 391 | * 不然没必要设置,点击后就没了 392 | */ 393 | FacileMenuItem *FacileMenuItem::textAfterClick(QString newText) 394 | { 395 | connect(this, &FacileMenuItem::clicked, this, [=]{ 396 | setText(newText); 397 | }); 398 | return this; 399 | } 400 | 401 | FacileMenuItem *FacileMenuItem::textAfterClick(FuncStringStringType func) 402 | { 403 | connect(this, &FacileMenuItem::clicked, this, [=]{ 404 | setText(func(this->InteractiveButtonBase::text)); 405 | }); 406 | return this; 407 | } 408 | 409 | /** 410 | * 适用于连续设置 411 | * 当 iff 成立时继续 412 | * 否则取消后面所有设置 413 | */ 414 | FacileMenuItem *FacileMenuItem::ifer(bool exp) 415 | { 416 | if (exp) 417 | return this; 418 | 419 | // 返回一个无用item,在自己delete时也delete掉 420 | return createTempItem(); 421 | } 422 | 423 | /** 424 | * 完全等于 ifer 425 | * 如果已经在 ifer 里面,则先退出 426 | */ 427 | FacileMenuItem *FacileMenuItem::elifer(bool exp) 428 | { 429 | if (parent_menu_item_in_if) // ifer 不成立后的,退出并转至新的 ifer 430 | return parent_menu_item_in_if->ifer(exp); 431 | return ifer(exp); // 直接使用,完全等同于 ifer 432 | } 433 | 434 | FacileMenuItem *FacileMenuItem::elser() 435 | { 436 | if (parent_menu_item_in_if) 437 | return parent_menu_item_in_if; 438 | return createTempItem(); 439 | } 440 | 441 | /** 442 | * 适用于连续设置action时,满足条件则退出 443 | * 相当于一个控制语句 444 | * 当ex成立时,取消后面所有设置 445 | */ 446 | FacileMenuItem *FacileMenuItem::exiter(bool exp) 447 | { 448 | if (!exp) 449 | return this; 450 | 451 | // 返回一个无用item,在自己delete时也delete掉 452 | return createTempItem(false); 453 | } 454 | 455 | /** 456 | * 适用于连续设置 457 | * 满足某一条件则执行 func(this) 458 | */ 459 | FacileMenuItem *FacileMenuItem::ifer(bool exp, FuncItemType func, FuncItemType elseFunc) 460 | { 461 | if (exp) 462 | { 463 | if (func) 464 | func(this); 465 | } 466 | else 467 | { 468 | if (elseFunc) 469 | elseFunc(this); 470 | } 471 | return this; 472 | } 473 | 474 | /** 475 | * 适用于连续设置 476 | * 类似 switch 语句,输入判断的值 477 | * 当后续的 caser 满足 value 时,允许执行 caser 的 func 或后面紧跟着的的设置 478 | */ 479 | FacileMenuItem *FacileMenuItem::switcher(int value) 480 | { 481 | switch_value = value; 482 | switch_matched = false; 483 | return this; 484 | } 485 | 486 | /** 487 | * 当 value 等同于 switcher 判断的 value 时,执行 func 488 | * 并返回原始 item 489 | * 注意与重载的 caser(int) 进行区分 490 | */ 491 | FacileMenuItem *FacileMenuItem::caser(int value, FuncType func) 492 | { 493 | if (value == switch_value) 494 | { 495 | switch_matched = true; 496 | if (func) 497 | func(); 498 | } 499 | return this; 500 | } 501 | 502 | /** 503 | * 当 value 等同于 switcher 的 value 时,返回原始 item 504 | * 即执行 caser 后面的设置,直至 breaker 505 | * 注意与重载的 caser(int FuncType) 进行区分 506 | */ 507 | FacileMenuItem *FacileMenuItem::caser(int value) 508 | { 509 | // 可能已经接着一个没有 breaker 的 caser 510 | // 则回到上一级(这样会导致无法嵌套) 511 | if (this->parent_menu_item_in_if) 512 | { 513 | // 接着一个 !=的caser 后面 514 | if (value == parent_menu_item_in_if->switch_value) 515 | { 516 | parent_menu_item_in_if->switch_matched = true; 517 | return parent_menu_item_in_if; // 真正需要使用的实例 518 | } 519 | return this; // 继续使用自己(一个临时实例) 520 | } 521 | else // 自己是第一个 caser 或者 ==的caser 后面 522 | { 523 | if (value == switch_value) 524 | { 525 | switch_matched = true; 526 | return this; 527 | } 528 | return createTempItem(); 529 | } 530 | } 531 | 532 | /** 533 | * caser 的 value 不等于 switcher 的 value 时 534 | * 此语句用来退出 535 | */ 536 | FacileMenuItem *FacileMenuItem::breaker() 537 | { 538 | if (parent_menu_item_in_if) 539 | return parent_menu_item_in_if; 540 | return this; // 应该不会吧…… 541 | } 542 | 543 | /** 544 | * 如果switcher的caser没有满足 545 | */ 546 | FacileMenuItem *FacileMenuItem::defaulter() 547 | { 548 | if (switch_matched) // 已经有 caser 匹配了 549 | return createTempItem(); // 返回无效临时实例 550 | return this; // 能用,返回自己 551 | } 552 | 553 | /** 554 | * 返回自己的子菜单对象 555 | */ 556 | FacileMenu *FacileMenuItem::subMenu() 557 | { 558 | return sub_menu; 559 | } 560 | 561 | void FacileMenuItem::paintEvent(QPaintEvent *event) 562 | { 563 | InteractiveButtonBase::paintEvent(event); 564 | 565 | int right = width()- 8; 566 | 567 | QPainter painter(this); 568 | if (isSubMenu()) 569 | { 570 | right -= icon_text_size; 571 | // 画右边箭头的图标 572 | QRect rect(right, fore_paddings.top, icon_text_size, icon_text_size); 573 | painter.drawPixmap(rect, QPixmap(":/icons/sub_menu_arrow")); 574 | } 575 | 576 | right -= icon_text_padding; 577 | if (!shortcut_tip.isEmpty()) 578 | { 579 | // 画右边的文字 580 | QFontMetrics fm(this->font()); 581 | int width = fm.horizontalAdvance(shortcut_tip); 582 | painter.save(); 583 | auto c = painter.pen().color(); 584 | c.setAlpha(c.alpha() / 2); 585 | painter.setPen(c); 586 | painter.drawText(QRect(right-width, fore_paddings.top, width, height()-fore_paddings.top-fore_paddings.bottom), 587 | Qt::AlignRight, shortcut_tip); 588 | painter.restore(); 589 | } 590 | } 591 | 592 | void FacileMenuItem::drawIconBeforeText(QPainter &painter, QRect icon_rect) 593 | { 594 | // 选中 595 | if (checkable) 596 | { 597 | QPainterPath path; 598 | QRect expand_rect = icon_rect; 599 | expand_rect.adjust(-2, -2, 2, 2); 600 | path.addRoundedRect(expand_rect, 3, 3); 601 | if (InteractiveButtonBase::icon.isNull()) 602 | { 603 | // 绘制√ 604 | if (isChecked()) 605 | painter.drawText(icon_rect, "√"); 606 | } 607 | else // 有图标,使用 608 | { 609 | if (getState()) 610 | { 611 | // 绘制选中样式: 圆角矩形 612 | painter.fillPath(path, press_bg); 613 | } 614 | else 615 | { 616 | // 绘制未选中样式:空白边框 617 | painter.save(); 618 | painter.setPen(QPen(press_bg, 1)); 619 | painter.drawPath(path); 620 | painter.restore(); 621 | } 622 | } 623 | } 624 | 625 | InteractiveButtonBase::drawIconBeforeText(painter, icon_rect); 626 | } 627 | 628 | FacileMenuItem *FacileMenuItem::createTempItem(bool thisIsParent) 629 | { 630 | auto useless = new FacileMenuItem(QIcon(), "", this); 631 | useless->parent_menu_item_in_if = thisIsParent ? this : nullptr; 632 | useless->hide(); 633 | useless->setEnabled(false); 634 | useless->setMinimumSize(0, 0); 635 | useless->setFixedSize(0, 0); 636 | useless->move(-999, -999); 637 | return useless; 638 | } 639 | -------------------------------------------------------------------------------- /facile_menu/facilemenuitem.h: -------------------------------------------------------------------------------- 1 | #ifndef FACILEMENUITEM_H 2 | #define FACILEMENUITEM_H 3 | 4 | #include "interactivebuttonbase.h" 5 | 6 | class FacileMenu; 7 | class FacileMenuItem; 8 | 9 | typedef std::function const FuncType; 10 | typedef std::function const FuncIntType; 11 | typedef std::function const FuncStringStringType; 12 | typedef std::function const FuncItemType; 13 | typedef std::function const FuncItemIntType; 14 | 15 | class FacileMenuItem : public InteractiveButtonBase 16 | { 17 | public: 18 | FacileMenuItem(QWidget* parent = nullptr); 19 | FacileMenuItem(QString t, QWidget* parent = nullptr); 20 | FacileMenuItem(QIcon i, QWidget* parent = nullptr); 21 | FacileMenuItem(QIcon i, QString t, QWidget* parent = nullptr); 22 | FacileMenuItem(QPixmap p, QString t, QWidget* parent = nullptr); 23 | 24 | FacileMenuItem* setEnabled(bool e); 25 | FacileMenuItem* setCheckable(bool c); 26 | bool isCheckable() const; 27 | FacileMenuItem* setChecked(bool c); 28 | bool isChecked(); 29 | FacileMenuItem* setKey(Qt::Key key); 30 | bool isKey(Qt::Key key) const; 31 | FacileMenuItem* setSubMenu(FacileMenu* menu); 32 | bool isSubMenu() const; 33 | bool isLinger() const; 34 | FacileMenuItem* setData(QVariant data); 35 | QVariant getData(); 36 | 37 | FacileMenuItem* tip(QString sc); 38 | FacileMenuItem* tip(bool exp, QString sc); 39 | FacileMenuItem* tooltip(QString tt); 40 | FacileMenuItem* tooltip(bool exp, QString tt); 41 | FacileMenuItem* triggered(FuncType func); 42 | FacileMenuItem* triggered(bool exp, FuncType func); 43 | FacileMenuItem* disable(bool exp = true); // 满足情况下触发,不满足不变,下同 44 | FacileMenuItem* enable(bool exp = true); 45 | FacileMenuItem* hide(bool exp = true); 46 | FacileMenuItem* visible(bool exp = true); 47 | FacileMenuItem* check(bool exp = true); 48 | FacileMenuItem* uncheck(bool exp = true); 49 | FacileMenuItem* toggle(bool exp = true); 50 | FacileMenuItem* autoToggle(); 51 | FacileMenuItem* text(bool exp, QString str); 52 | FacileMenuItem* text(bool exp, QString tru, QString fal); 53 | FacileMenuItem* fgColor(QColor color); 54 | FacileMenuItem* fgColor(bool exp, QColor color); 55 | FacileMenuItem* bgColor(QColor color); 56 | FacileMenuItem* bgColor(bool exp, QColor color); 57 | FacileMenuItem* prefix(bool exp, QString pfix); 58 | FacileMenuItem* suffix(bool exp, QString sfix, bool inLeftParenthesis = true); 59 | FacileMenuItem* prefix(QString pfix); 60 | FacileMenuItem* suffix(QString sfix, bool inLeftParenthesis = true); 61 | FacileMenuItem* icon(bool exp, QIcon icon); 62 | FacileMenuItem* borderR(int radius = 3, QColor co = Qt::transparent); 63 | FacileMenuItem* linger(); 64 | FacileMenuItem* lingerText(QString textAfterClick); 65 | FacileMenuItem* bind(bool &val); 66 | FacileMenuItem* longPress(FuncType func); 67 | FacileMenuItem* textAfterClick(QString newText); 68 | FacileMenuItem* textAfterClick(FuncStringStringType func); 69 | 70 | FacileMenuItem* ifer(bool exp); 71 | FacileMenuItem* elifer(bool exp); 72 | FacileMenuItem* elser(); 73 | FacileMenuItem* exiter(bool exp = true); 74 | FacileMenuItem* ifer(bool exp, FuncItemType func, FuncItemType elseFunc = nullptr); 75 | FacileMenuItem* switcher(int value); 76 | FacileMenuItem* caser(int value, FuncType func); 77 | FacileMenuItem* caser(int value); 78 | FacileMenuItem* breaker(); 79 | FacileMenuItem* defaulter(); 80 | 81 | FacileMenu* subMenu(); 82 | 83 | protected: 84 | void paintEvent(QPaintEvent *event) override; 85 | void drawIconBeforeText(QPainter &painter, QRect icon_rect) override; 86 | 87 | FacileMenuItem* createTempItem(bool thisIsParent = true); 88 | 89 | private: 90 | Qt::Key key; 91 | bool checkable = false; 92 | bool trigger_linger = false; // 点击后是否保存菜单 93 | FacileMenu* sub_menu = nullptr; 94 | QString shortcut_tip = ""; // 快捷键提示 95 | FacileMenuItem* parent_menu_item_in_if = nullptr; // elser/caser专用 96 | int switch_value = 0; // switcher的值,用来和caser比较(不需要breaker……) 97 | bool switch_matched = true; 98 | QVariant data; 99 | }; 100 | 101 | #endif // FACILEMENUITEM_H 102 | -------------------------------------------------------------------------------- /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.2), 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 | { 99 | model = PaintModel::Text; 100 | setAlign(Qt::AlignCenter); 101 | } 102 | else if (model == PaintModel::PixmapMask) 103 | { 104 | if (pixmap.isNull()) 105 | model = PaintModel::Text; 106 | else 107 | model = PaintModel::PixmapText; 108 | setAlign(Qt::AlignLeft | Qt::AlignVCenter); 109 | QFontMetrics fm(this->font()); 110 | icon_text_size = fm.lineSpacing(); 111 | } 112 | else if (model == PaintModel::Icon) 113 | { 114 | if (text.isEmpty()) 115 | { 116 | model = PaintModel::Icon; 117 | setAlign(Qt::AlignCenter); 118 | } 119 | else 120 | { 121 | if (icon.isNull()) 122 | model = PaintModel::Text; 123 | else 124 | model = PaintModel::IconText; 125 | setAlign(Qt::AlignLeft | Qt::AlignVCenter); 126 | QFontMetrics fm(this->font()); 127 | icon_text_size = fm.lineSpacing(); 128 | } 129 | } 130 | 131 | if (parent_enabled) 132 | QPushButton::setText(text); 133 | 134 | // 根据字体调整大小 135 | if (text_dynamic_size) 136 | { 137 | adjustMinimumSize(); 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 | { 168 | model = PaintModel::Icon; 169 | setAlign(Qt::AlignCenter); 170 | } 171 | else if (model == PaintModel::Text) 172 | { 173 | if (text.isEmpty()) 174 | model = PaintModel::Icon; 175 | else 176 | model = PaintModel::IconText; 177 | setAlign(Qt::AlignLeft | Qt::AlignVCenter); 178 | QFontMetrics fm(this->font()); 179 | icon_text_size = fm.lineSpacing(); 180 | } 181 | else if (model == PaintModel::PixmapMask) 182 | { 183 | pixmap = QPixmap(); 184 | model = PaintModel::Icon; 185 | } 186 | else if (model == PaintModel::PixmapText) 187 | { 188 | pixmap = QPixmap(); 189 | if (text.isEmpty()) 190 | model = PaintModel::Icon; 191 | else 192 | model = PaintModel::IconText; 193 | setAlign(Qt::AlignLeft | Qt::AlignVCenter); 194 | QFontMetrics fm(this->font()); 195 | icon_text_size = fm.lineSpacing(); 196 | } 197 | else if (model == PaintModel::IconText && text.isEmpty()) 198 | { 199 | model = PaintModel::Icon; 200 | setAlign(Qt::AlignCenter); 201 | } 202 | this->icon = icon; 203 | if (parent_enabled) 204 | QPushButton::setIcon(icon); 205 | update(); 206 | } 207 | 208 | /** 209 | * 设置 Pixmap 210 | * @param pixmap [description] 211 | */ 212 | void InteractiveButtonBase::setPixmap(QPixmap pixmap) 213 | { 214 | if (model == PaintModel::None) 215 | model = PaintModel::PixmapMask; 216 | else if (model == PaintModel::Text) 217 | { 218 | if (text.isEmpty()) 219 | model = PaintModel::PixmapMask; 220 | else 221 | model = PaintModel::PixmapText; 222 | setAlign(Qt::AlignLeft | Qt::AlignVCenter); 223 | QFontMetrics fm(this->font()); 224 | icon_text_size = fm.lineSpacing(); 225 | } 226 | else if (model == PaintModel::Icon) 227 | { 228 | icon = QIcon(); 229 | model = PaintModel::PixmapMask; 230 | } 231 | else if (model == PaintModel::IconText) 232 | { 233 | icon = QIcon(); 234 | if (text.isEmpty()) 235 | model = PaintModel::PixmapMask; 236 | else 237 | model = PaintModel::PixmapText; 238 | setAlign(Qt::AlignLeft | Qt::AlignVCenter); 239 | QFontMetrics fm(this->font()); 240 | icon_text_size = fm.lineSpacing(); 241 | } 242 | this->pixmap = getMaskPixmap(pixmap, isEnabled() ? icon_color : getOpacityColor(icon_color)); 243 | if (parent_enabled) 244 | QPushButton::setIcon(QIcon(pixmap)); 245 | update(); 246 | } 247 | 248 | /** 249 | * 设置额外的图标,例如角标 250 | * @param pixmap 图标 251 | * @param align 对齐方式 252 | * @param size 图标尺寸 253 | */ 254 | void InteractiveButtonBase::setPaintAddin(QPixmap pixmap, Qt::Alignment align, QSize size) 255 | { 256 | QBitmap mask = pixmap.mask(); 257 | pixmap.fill(icon_color); 258 | pixmap.setMask(mask); 259 | paint_addin = PaintAddin(pixmap, align, size); 260 | update(); 261 | } 262 | 263 | /** 264 | * 设置子类功能是否开启 265 | * 如果关闭,则相当于默认的 QPushButton 266 | * @param e 开关 267 | */ 268 | void InteractiveButtonBase::setSelfEnabled(bool e) 269 | { 270 | self_enabled = e; 271 | } 272 | 273 | /** 274 | * 设置父类(QPushButton)功能是否开启 275 | * 如果开启,则绘制父类背景、父类前景 276 | * @param e 开关 277 | */ 278 | void InteractiveButtonBase::setParentEnabled(bool e) 279 | { 280 | parent_enabled = e; 281 | 282 | // 传递子类内容到父类去,避免子类关掉后不显示 283 | if (model == PaintModel::Text || model == PaintModel::IconText || model == PaintModel::PixmapText) 284 | QPushButton::setText(text); 285 | if (model == PaintModel::Icon || model == PaintModel::IconText) 286 | QPushButton::setIcon(icon); 287 | if (model == PaintModel::PixmapMask || model == PaintModel::PixmapText) 288 | QPushButton::setIcon(QIcon(pixmap)); 289 | } 290 | 291 | /** 292 | * 设置是否绘制前景图标/文字 293 | * 关闭后则只绘制背景 294 | * @param e 开启 295 | */ 296 | void InteractiveButtonBase::setForeEnabled(bool e) 297 | { 298 | fore_enabled = e; 299 | } 300 | 301 | /** 302 | * 设置鼠标悬浮背景渐变的动画时长 303 | * @param d 动画时长(毫秒) 304 | */ 305 | void InteractiveButtonBase::setHoverAniDuration(int d) 306 | { 307 | this->hover_bg_duration = d; 308 | // hover_progress = 0; // 重置hover效果 309 | } 310 | 311 | /** 312 | * 设置鼠标按下渐变效果的动画时长 313 | * @param d 动画时长(毫秒) 314 | */ 315 | void InteractiveButtonBase::setPressAniDuration(int d) 316 | { 317 | this->press_bg_duration = d; 318 | } 319 | 320 | /** 321 | * 设置单击效果的动画时长 322 | * @param d 动画时长(毫秒) 323 | */ 324 | void InteractiveButtonBase::setClickAniDuration(int d) 325 | { 326 | this->click_ani_duration = d; 327 | } 328 | 329 | /** 330 | * 设置水波纹动画时长 331 | * @param press 按住时时长(时长毫秒) 332 | * @param release 松开后速度(时长毫秒) 333 | * @param finish 渐变消失速度(时长毫秒) 334 | */ 335 | void InteractiveButtonBase::setWaterAniDuration(int press, int release, int finish) 336 | { 337 | this->water_press_duration = press; 338 | this->water_release_duration = release; 339 | this->water_finish_duration = finish; 340 | } 341 | 342 | /** 343 | * 各种状态改变 344 | * 主要是监控 可用 状态,不可用时设置为半透明 345 | */ 346 | void InteractiveButtonBase::changeEvent(QEvent *event) 347 | { 348 | QPushButton::changeEvent(event); 349 | 350 | if (event->type() == QEvent::EnabledChange && model == PixmapMask) // 可用状态改变了 351 | { 352 | if (isEnabled()) // 恢复可用:透明度变回去 353 | { 354 | QColor color = icon_color; 355 | color.setAlpha(color.alpha() * 2); 356 | setIconColor(color); 357 | } 358 | else // 变成不可用:透明度减半 359 | { 360 | QColor color = icon_color; 361 | color.setAlpha(color.alpha() / 2); 362 | setIconColor(color); 363 | } 364 | } 365 | } 366 | 367 | /** 368 | * 设置水波纹动画是否开启 369 | * 关闭时,将使用渐变动画 370 | * @param enable 开关 371 | */ 372 | void InteractiveButtonBase::setWaterRipple(bool enable) 373 | { 374 | if (water_animation == enable) 375 | return; 376 | water_animation = enable; 377 | } 378 | 379 | /** 380 | * 设置抖动效果是否开启 381 | * 鼠标拖拽移动的距离越长,抖动距离越长、次数越多 382 | * @param enable 开关 383 | */ 384 | void InteractiveButtonBase::setJitterAni(bool enable) 385 | { 386 | jitter_animation = enable; 387 | } 388 | 389 | /** 390 | * 设置是否使用统一图标绘制区域 391 | * 监听图标尺寸大小变化、中心点偏移,计算新的中心坐标位置 392 | * @param enable 开关 393 | */ 394 | void InteractiveButtonBase::setUnifyGeomerey(bool enable) 395 | { 396 | unified_geometry = enable; 397 | _l = _t = 0; 398 | _w = width(); 399 | _h = height(); 400 | } 401 | 402 | /** 403 | * 设置背景颜色 404 | * @param bg 背景颜色 405 | */ 406 | void InteractiveButtonBase::setBgColor(QColor bg) 407 | { 408 | setNormalColor(bg); 409 | update(); 410 | } 411 | 412 | /** 413 | * 设置事件背景颜色 414 | * @param hover 鼠标悬浮时的背景颜色 415 | * @param press 鼠标按下时的背景颜色 416 | */ 417 | void InteractiveButtonBase::setBgColor(QColor hover, QColor press) 418 | { 419 | if (hover != Qt::black) 420 | setHoverColor(hover); 421 | if (press != Qt::black) 422 | setPressColor(press); 423 | update(); 424 | } 425 | 426 | /** 427 | * 设置按钮背景颜色 428 | * @param color 背景颜色 429 | */ 430 | void InteractiveButtonBase::setNormalColor(QColor color) 431 | { 432 | normal_bg = color; 433 | update(); 434 | } 435 | 436 | /** 437 | * 设置边框线条颜色 438 | * @param color 边框颜色 439 | */ 440 | void InteractiveButtonBase::setBorderColor(QColor color) 441 | { 442 | border_bg = color; 443 | } 444 | 445 | /** 446 | * 设置鼠标悬浮时的背景颜色 447 | * @param color 背景颜色 448 | */ 449 | void InteractiveButtonBase::setHoverColor(QColor color) 450 | { 451 | hover_bg = color; 452 | } 453 | 454 | /** 455 | * 设置鼠标按住时的背景颜色 456 | * @param color 背景颜色 457 | */ 458 | void InteractiveButtonBase::setPressColor(QColor color) 459 | { 460 | press_bg = color; 461 | } 462 | 463 | /** 464 | * 设置图标颜色(仅针对可变色的 pixmap 图标) 465 | * @param color 图标颜色 466 | */ 467 | void InteractiveButtonBase::setIconColor(QColor color) 468 | { 469 | icon_color = color; 470 | 471 | // 绘制图标(如果有) 472 | if (model == PaintModel::PixmapMask || model == PaintModel::PixmapText) 473 | { 474 | pixmap = getMaskPixmap(pixmap, isEnabled() ? icon_color : getOpacityColor(icon_color)); 475 | } 476 | 477 | // 绘制额外角标(如果有的话) 478 | if (paint_addin.enable) 479 | { 480 | paint_addin.pixmap = getMaskPixmap(paint_addin.pixmap, isEnabled() ? icon_color : getOpacityColor(icon_color)); 481 | } 482 | 483 | update(); 484 | } 485 | 486 | /** 487 | * 设置前景文字颜色 488 | * @param color 文字颜色 489 | */ 490 | void InteractiveButtonBase::setTextColor(QColor color) 491 | { 492 | text_color = color; 493 | update(); 494 | } 495 | 496 | /** 497 | * 设置获取焦点时的背景颜色(默认关闭) 498 | * @param color 背景颜色 499 | */ 500 | void InteractiveButtonBase::setFocusBg(QColor color) 501 | { 502 | setFocusPolicy(Qt::StrongFocus); 503 | focus_bg = color; 504 | } 505 | 506 | /** 507 | * 设置获取焦点时的边框颜色(默认关闭) 508 | * @param color 边框颜色 509 | */ 510 | void InteractiveButtonBase::setFocusBorder(QColor color) 511 | { 512 | setFocusPolicy(Qt::StrongFocus); 513 | focus_border = color; 514 | } 515 | 516 | /** 517 | * 设置文字大小(PointSize,覆盖 font() 字体大小) 518 | * @param f 文字大小 519 | */ 520 | void InteractiveButtonBase::setFontSize(int f) 521 | { 522 | if (!font_size) // 第一次设置字体大小,直接设置 523 | { 524 | font_size = f; 525 | QFont font(this->font()); 526 | font.setPointSize(f); 527 | setFont(font); 528 | update(); 529 | } 530 | else // 改变字体大小,使用字体缩放动画 531 | { 532 | QPropertyAnimation *ani = new QPropertyAnimation(this, "font_size"); 533 | ani->setStartValue(font_size); 534 | ani->setEndValue(f); 535 | ani->setDuration(click_ani_duration); 536 | connect(ani, &QPropertyAnimation::finished, [=] { 537 | QFontMetrics fm(this->font()); 538 | icon_text_size = fm.lineSpacing(); 539 | ani->deleteLater(); 540 | }); 541 | ani->start(); 542 | } 543 | // 修改字体大小时调整按钮的最小尺寸,避免文字显示不全 544 | if (text_dynamic_size) 545 | { 546 | QFont font; 547 | font.setPointSize(f); 548 | QFontMetrics fms(font); 549 | setMinimumSize(fms.horizontalAdvance(text) + fore_paddings.left + fore_paddings.right, fms.lineSpacing() + fore_paddings.top + fore_paddings.bottom); 550 | } 551 | if (model != PaintModel::Text) 552 | { 553 | QFontMetrics fm(this->font()); 554 | icon_text_size = fm.lineSpacing(); 555 | } 556 | } 557 | 558 | /** 559 | * 获取字体大小 560 | * 用来作为字体动画的属性参数 561 | * @return 临时字体大小 562 | */ 563 | int InteractiveButtonBase::getFontSizeT() 564 | { 565 | return font_size; 566 | } 567 | 568 | /** 569 | * 设置动画中的临时字体大小 570 | * 用来作为字体动画的属性参数 571 | * @param f 临时字体大小 572 | */ 573 | void InteractiveButtonBase::setFontSizeT(int f) 574 | { 575 | this->font_size = f; 576 | QFont font(this->font()); 577 | font.setPointSize(f); 578 | setFont(font); 579 | update(); 580 | } 581 | 582 | /** 583 | * 如果点击失去焦点的话,即使鼠标移到上面,也不会出现背景 584 | * 可以用这个方法继续保持悬浮状态 585 | */ 586 | void InteractiveButtonBase::setHover() 587 | { 588 | if (!hovering && inArea(mapFromGlobal(QCursor::pos()))) 589 | InteractiveButtonBase::enterEvent(new QEvent(QEvent::Type::None)); 590 | } 591 | 592 | /** 593 | * 设置对齐方式 594 | * @param a 对齐方式 595 | */ 596 | void InteractiveButtonBase::setAlign(Qt::Alignment a) 597 | { 598 | align = a; 599 | update(); 600 | } 601 | 602 | /** 603 | * 设置四个角的半径 604 | * @param r 半径 605 | */ 606 | void InteractiveButtonBase::setRadius(int r) 607 | { 608 | radius_x = radius_y = r; 609 | } 610 | 611 | /** 612 | * 分开设置 X、Y 的半径 613 | * @param rx X半径 614 | * @param ry Y半径 615 | */ 616 | void InteractiveButtonBase::setRadius(int rx, int ry) 617 | { 618 | radius_x = rx; 619 | radius_y = ry; 620 | } 621 | 622 | /** 623 | * 设置边框线条的粗细 624 | * @param x 线条粗细 625 | */ 626 | void InteractiveButtonBase::setBorderWidth(int x) 627 | { 628 | border_width = x; 629 | } 630 | 631 | /** 632 | * 设置不可用情况(默认为假) 633 | * 区别于 setEnabled(bool),两个相反的,并不是覆盖方法 634 | * @param dis 不可用 635 | */ 636 | void InteractiveButtonBase::setDisabled(bool dis) 637 | { 638 | if (dis == !isEnabled()) // 相同的 639 | return; 640 | 641 | setEnabled(!dis); 642 | 643 | if (parentWidget() != nullptr) 644 | { 645 | setAttribute(Qt::WA_TransparentForMouseEvents, dis); // 点击穿透 646 | } 647 | 648 | if (model == PixmapMask || model == PixmapText) 649 | { 650 | pixmap = getMaskPixmap(pixmap, dis ? getOpacityColor(icon_color) : icon_color); 651 | } 652 | 653 | update(); // 修改透明度 654 | } 655 | 656 | /** 657 | * 设置前景和四条边的 paddings 658 | * @param l 左边空白 659 | * @param r 右边空白 660 | * @param t 顶边空白 661 | * @param b 底边空白 662 | */ 663 | void InteractiveButtonBase::setPaddings(int l, int r, int t, int b) 664 | { 665 | fore_paddings.left = l; 666 | fore_paddings.right = r; 667 | fore_paddings.top = t; 668 | fore_paddings.bottom = b; 669 | setFixedForeSize(); 670 | } 671 | 672 | /** 673 | * 统一设置方向的 paddings 674 | * @param h 横向 675 | * @param v 纵向 676 | */ 677 | void InteractiveButtonBase::setPaddings(int h, int v) 678 | { 679 | fore_paddings.left = fore_paddings.right = (h + 1) / 2; 680 | fore_paddings.top = fore_paddings.bottom = (v + 1) / 2; 681 | setFixedForeSize(); 682 | } 683 | 684 | /** 685 | * 统一设置前景和四条边的 paddings 686 | * @param x 一样大小的四边留白 687 | */ 688 | void InteractiveButtonBase::setPaddings(int x) 689 | { 690 | fore_paddings.left = x; 691 | fore_paddings.right = x; 692 | fore_paddings.top = x; 693 | fore_paddings.bottom = x; 694 | setFixedForeSize(); 695 | } 696 | 697 | /** 698 | * 设置Icon模式旁边空多少 699 | * @param x 0~1.0,越大越空 700 | */ 701 | void InteractiveButtonBase::setIconPaddingProper(double x) 702 | { 703 | icon_padding_proper = x; 704 | int short_side = min(width(), height()); // 短边 705 | // 非固定的情况,尺寸大小变了之后所有 padding 都要变 706 | int padding = int(short_side * icon_padding_proper); //static_cast(short_side * (1 - GOLDEN_RATIO) / 2); 707 | fore_paddings.left = fore_paddings.top = fore_paddings.right = fore_paddings.bottom = padding; 708 | update(); 709 | } 710 | 711 | /** 712 | * 设置字体大小时是否同步修改按钮的最小尺寸(避免按钮显示不全) 713 | * @param d 开关 714 | */ 715 | void InteractiveButtonBase::setTextDynamicSize(bool d) 716 | { 717 | text_dynamic_size = d; 718 | } 719 | 720 | /** 721 | * 见 setFixedForePos(bool f) 722 | */ 723 | void InteractiveButtonBase::setFixedTextPos(bool f) 724 | { 725 | fixed_fore_pos = f; 726 | } 727 | 728 | /** 729 | * 设置前景是否固定,而不移动 730 | * 将去除鼠标移入靠近、抖动效果,统一图标区域大小不变 731 | * 只包括:鼠标进入/点击,均表现为缩放效果(默认) 732 | * 不影响任何其他功能 733 | * @param f [description] 734 | */ 735 | void InteractiveButtonBase::setFixedForePos(bool f) 736 | { 737 | fixed_fore_pos = f; 738 | } 739 | 740 | /** 741 | * 固定按钮(最小值)为适当尺寸,并且固定四周留白 742 | * 前景应为文字/图标对应尺寸的最小尺寸 743 | * @param f 是否固定前景 744 | * @param addin 留白的像素大小 745 | */ 746 | void InteractiveButtonBase::setFixedForeSize(bool f, int addin) 747 | { 748 | fixed_fore_size = f; 749 | 750 | if (!f) 751 | return; 752 | if (model == PaintModel::Text || model == PaintModel::IconText || model == PaintModel::PixmapText) 753 | { 754 | int icon_width = (model != PaintModel::Text && icon.isNull()) ? 0 : icon_text_size; 755 | QFont font = this->font(); 756 | if (font_size > 0) 757 | font.setPointSize(font_size); 758 | QFontMetrics fm(font); 759 | int w = fm.horizontalAdvance(text); 760 | w = icon_width + w + quick_sqrt(w / 2) + fore_paddings.left + fore_paddings.right; 761 | setMinimumSize( 762 | w + addin, 763 | fm.lineSpacing() + fore_paddings.top + fore_paddings.bottom + addin); 764 | } 765 | else if (model == PaintModel::Icon || model == PaintModel::PixmapMask) 766 | { 767 | int size = height(); 768 | int m = qMax(qMax(fore_paddings.left, fore_paddings.right), qMax(fore_paddings.top, fore_paddings.bottom)); 769 | setMinimumSize(size + addin + m, size + addin + m); 770 | } 771 | } 772 | 773 | void InteractiveButtonBase::setSquareSize() 774 | { 775 | setFixedWidth(height()); 776 | setMinimumWidth(height()); 777 | setMaximumWidth(height()); 778 | } 779 | 780 | /** 781 | * 设置鼠标单击松开后是否当做移开 782 | * 避免菜单、弹窗出现后,由于鼠标仍然留在按钮上面,导致依旧显示 hover 背景 783 | * @param l 开关 784 | */ 785 | void InteractiveButtonBase::setLeaveAfterClick(bool l) 786 | { 787 | leave_after_clicked = l; 788 | } 789 | 790 | /** 791 | * 响应双击事件 792 | * 注意:会先触发单击事件、再触发双击事件(其实就是懒得做) 793 | * 建议在 QListWidget 等地方使用! 794 | * @param e 开关 795 | */ 796 | void InteractiveButtonBase::setDoubleClicked(bool e) 797 | { 798 | double_clicked = e; 799 | 800 | if (double_timer == nullptr) 801 | { 802 | double_timer = new QTimer(this); 803 | double_timer->setInterval(DOUBLE_PRESS_INTERVAL); 804 | connect(double_timer, &QTimer::timeout, [=] { 805 | double_timer->stop(); 806 | emit clicked(); // 手动触发单击事件 807 | }); 808 | } 809 | } 810 | 811 | /** 812 | * 动画时是否自动设置文字的颜色 813 | */ 814 | void InteractiveButtonBase::setAutoTextColor(bool a) 815 | { 816 | this->auto_text_color = a; 817 | } 818 | 819 | /** 820 | * 一开始没有聚焦时,假装获取焦点 821 | * 通过信号槽使其他控件(例如QLineEdit)按下enter键触发此按钮事件 822 | * 直到触发了焦点改变事件,此控件失去焦点(需要手动改变) 823 | */ 824 | void InteractiveButtonBase::setPretendFocus(bool f) 825 | { 826 | focusing = f; 827 | update(); 828 | } 829 | 830 | /** 831 | * 如果按钮被做成一个组合,在显示的时候开启动画 832 | * 一开始鼠标下的按钮一直在hover状态,移开也不会变 833 | * 开启后临时屏蔽,记得在动画结束后关闭 834 | */ 835 | void InteractiveButtonBase::setBlockHover(bool b) 836 | { 837 | _block_hover = b; 838 | if (b && hovering) 839 | leaveEvent(nullptr); 840 | } 841 | 842 | /** 843 | * 是否开启出现动画 844 | * 鼠标进入按钮区域,前景图标从对面方向缩放出现 845 | * @param enable 开关 846 | */ 847 | void InteractiveButtonBase::setShowAni(bool enable) 848 | { 849 | show_animation = enable; 850 | 851 | if (!show_animation) // 关闭隐藏前景 852 | { 853 | show_foreground = true; 854 | } 855 | else if (show_animation) // 开启隐藏前景 856 | { 857 | if (!hovering && !pressing) // 应该是隐藏状态 858 | { 859 | show_ani_appearing = show_ani_disappearing = show_foreground = false; 860 | show_ani_progress = 0; 861 | } 862 | else // 应该是显示状态 863 | { 864 | show_foreground = true; 865 | show_ani_appearing = show_ani_disappearing = false; 866 | show_ani_progress = 100; 867 | } 868 | } 869 | } 870 | 871 | /** 872 | * 按钮前景出现动画 873 | * 从中心点出现的缩放动画 874 | */ 875 | void InteractiveButtonBase::showForeground() 876 | { 877 | if (!show_animation) 878 | return; 879 | waters.clear(); 880 | if (!anchor_timer->isActive()) 881 | anchor_timer->start(); 882 | if (show_ani_disappearing) 883 | show_ani_disappearing = false; 884 | show_ani_appearing = true; 885 | show_timestamp = getTimestamp(); 886 | show_foreground = true; 887 | show_ani_point = QPoint(0, 0); 888 | } 889 | 890 | /** 891 | * 按钮前景出现动画2 892 | * 指定方向(笛卡尔坐标),从反方向至中心点 893 | * @param point 最开始出现的方向(大小不影响,只按 x、y 比例来) 894 | */ 895 | void InteractiveButtonBase::showForeground2(QPoint point) 896 | { 897 | showForeground(); 898 | if (point == QPoint(0, 0)) 899 | point = mapFromGlobal(QCursor::pos()) - QPoint(width() / 2, height() / 2); // 相对于按钮中心 900 | show_ani_point = point; 901 | 902 | if (unified_geometry) // 统一出现动画 903 | updateUnifiedGeometry(); 904 | } 905 | 906 | /** 907 | * 隐藏前景 908 | * 为下一次的出现动画做准备 909 | */ 910 | void InteractiveButtonBase::hideForeground() 911 | { 912 | if (!show_animation) 913 | return; 914 | if (!anchor_timer->isActive()) 915 | anchor_timer->start(); 916 | if (show_ani_appearing) 917 | show_ani_appearing = false; 918 | show_ani_disappearing = true; 919 | hide_timestamp = getTimestamp(); 920 | } 921 | 922 | /** 923 | * 延迟出现前景 924 | * 适用于多个按钮连续出现的一套效果 925 | * @param time 延迟时长(毫秒) 926 | * @param point 出现方向 927 | */ 928 | void InteractiveButtonBase::delayShowed(int time, QPoint point) 929 | { 930 | setShowAni(true); 931 | QTimer::singleShot(time, [=] { 932 | showForeground2(point); 933 | connect(this, &InteractiveButtonBase::showAniFinished, [=] { 934 | setShowAni(false); 935 | disconnect(this, SIGNAL(showAniFinished()), nullptr, nullptr); 936 | }); 937 | }); 938 | } 939 | 940 | /** 941 | * 获取文字 942 | */ 943 | QString InteractiveButtonBase::getText() 944 | { 945 | return text; 946 | } 947 | 948 | /** 949 | * 设置菜单 950 | * 并解决菜单无法监听到 release 的问题 951 | * @param menu 菜单对象 952 | */ 953 | void InteractiveButtonBase::setMenu(QMenu *menu) 954 | { 955 | // 默认设置了不获取焦点事件,所以如果设置了菜单的话,就不会有Release事件,水波纹动画会一直飘荡 956 | // 在 focusOut 事件中,模拟了 release 事件, 957 | this->setFocusPolicy(Qt::FocusPolicy::ClickFocus); 958 | 959 | QPushButton::setMenu(menu); 960 | } 961 | 962 | /** 963 | * 根据内容,调整最小尺寸(不影响最大值) 964 | */ 965 | void InteractiveButtonBase::adjustMinimumSize() 966 | { 967 | int icon_width = (model == PaintModel::Text || model == PaintModel::None) || icon.isNull() ? 0 : icon_text_size; 968 | if (icon_width && !text.isEmpty()) 969 | icon_width += icon_text_padding; 970 | int w = 0, h = 0; 971 | if (font_size <= 0) 972 | { 973 | QFontMetrics fm(font()); 974 | w = fm.horizontalAdvance(text); 975 | w = icon_width + w + quick_sqrt(w / 2) + fore_paddings.left + fore_paddings.right; 976 | h = fm.lineSpacing() + fore_paddings.top + fore_paddings.bottom; 977 | } 978 | else 979 | { 980 | QFont font; 981 | font.setPointSize(font_size); 982 | QFontMetrics fm(font); 983 | w = fm.horizontalAdvance(text); 984 | w = icon_width + w + quick_sqrt(w / 2) + fore_paddings.left + fore_paddings.right; 985 | h = fm.lineSpacing() + fore_paddings.top + fore_paddings.bottom; 986 | } 987 | setMinimumSize(w, h); 988 | } 989 | 990 | /** 991 | * 设置状态 992 | * 一个用来作为开关效果的属性 993 | * @param s 状态 994 | */ 995 | void InteractiveButtonBase::setState(bool s) 996 | { 997 | _state = s; 998 | update(); 999 | } 1000 | 1001 | /** 1002 | * 获取状态 1003 | * @return 状态 1004 | */ 1005 | bool InteractiveButtonBase::getState() 1006 | { 1007 | return _state; 1008 | } 1009 | 1010 | /** 1011 | * 模拟按下开关的效果,并改变状态 1012 | * 如果不使用状态,则出现点击动画 1013 | * @param s 目标状态(默认为false) 1014 | * @param a 鼠标在区域内则点击无效(恐怕再次点击) 1015 | */ 1016 | void InteractiveButtonBase::simulateStatePress(bool s, bool a) 1017 | { 1018 | if (getState() == s) 1019 | return; 1020 | 1021 | // 鼠标悬浮在上方,有两种情况: 1022 | // 1、点击按钮后触发,重复了 1023 | // 2、需要假装触发,例如 Popup 类型,尽管悬浮在上面,但是无法点击到 1024 | if (a && inArea(mapFromGlobal(QCursor::pos()))) // 点击当前按钮,不需要再模拟了 1025 | return; 1026 | 1027 | mousePressEvent(new QMouseEvent(QMouseEvent::Type::None, QPoint(width() / 2, height() / 2), Qt::LeftButton, Qt::NoButton, Qt::NoModifier)); 1028 | 1029 | mouseReleaseEvent(new QMouseEvent(QMouseEvent::Type::None, QPoint(width() / 2, height() / 2), Qt::LeftButton, Qt::NoButton, Qt::NoModifier)); 1030 | 1031 | // if (!inArea(mapFromGlobal(QCursor::pos()))) // 针对模拟release 后面 // 必定成立 1032 | hovering = false; 1033 | } 1034 | 1035 | /** 1036 | * 模拟鼠标悬浮的效果 1037 | * 适用于键盘操作时,模拟鼠标hover状态 1038 | * 用 discardHoverPress 取消状态 1039 | */ 1040 | void InteractiveButtonBase::simulateHover() 1041 | { 1042 | if (!hovering) 1043 | { 1044 | if (_block_hover) 1045 | setBlockHover(false); // 可能已经临时屏蔽掉鼠标 enter 事件,强制hover 1046 | enterEvent(nullptr); 1047 | } 1048 | } 1049 | 1050 | /** 1051 | * 强制丢弃hover、press状态 1052 | * 适用于悬浮/点击后,弹出模态浮窗 1053 | * 浮窗关闭后调用此方法 1054 | * @param force 如果鼠标仍在此按钮内,是否强制取消hover/press状态 1055 | */ 1056 | void InteractiveButtonBase::discardHoverPress(bool force) 1057 | { 1058 | if (!force && inArea(mapFromGlobal(QCursor::pos()))) // 鼠标还在这范围内 1059 | return; 1060 | 1061 | if (hovering) 1062 | { 1063 | leaveEvent(nullptr); 1064 | } 1065 | 1066 | if (pressing) 1067 | { 1068 | mouseReleaseEvent(new QMouseEvent(QMouseEvent::Type::None, QPoint(width() / 2, height() / 2), Qt::LeftButton, Qt::NoButton, Qt::NoModifier)); 1069 | } 1070 | } 1071 | 1072 | /** 1073 | * 鼠标移入事件,触发 hover 时间戳 1074 | */ 1075 | void InteractiveButtonBase::enterEvent(QEvent *event) 1076 | { 1077 | if (_block_hover) // 临时屏蔽hover事件 1078 | { 1079 | if (event) 1080 | event->accept(); 1081 | return; 1082 | } 1083 | 1084 | if (!anchor_timer->isActive()) 1085 | { 1086 | anchor_timer->start(); 1087 | } 1088 | hovering = true; 1089 | hover_timestamp = getTimestamp(); 1090 | leave_timestamp = 0; 1091 | if (mouse_pos == QPoint(-1, -1)) 1092 | mouse_pos = mapFromGlobal(QCursor::pos()); 1093 | emit signalMouseEnter(); 1094 | 1095 | return QPushButton::enterEvent(event); 1096 | } 1097 | 1098 | /** 1099 | * 鼠标移开事件,触发 leave 时间戳 1100 | */ 1101 | void InteractiveButtonBase::leaveEvent(QEvent *event) 1102 | { 1103 | hovering = false; 1104 | if (!pressing) 1105 | mouse_pos = QPoint(width() / 2, height() / 2); 1106 | emit signalMouseLeave(); 1107 | 1108 | return QPushButton::leaveEvent(event); 1109 | } 1110 | 1111 | /** 1112 | * 鼠标按下事件,触发 press 时间戳 1113 | * 添加水波纹动画 waters 队列 1114 | */ 1115 | void InteractiveButtonBase::mousePressEvent(QMouseEvent *event) 1116 | { 1117 | mouse_pos = event->pos(); 1118 | 1119 | if (event->button() == Qt::LeftButton) 1120 | { 1121 | if (!hovering) 1122 | InteractiveButtonBase::enterEvent(new QEvent(QEvent::Type::None)); 1123 | 1124 | pressing = true; 1125 | press_pos = mouse_pos; 1126 | // 判断双击事件 1127 | if (double_clicked) 1128 | { 1129 | qint64 last_press_timestamp = press_timestamp; 1130 | press_timestamp = getTimestamp(); 1131 | if (release_timestamp + DOUBLE_PRESS_INTERVAL >= press_timestamp && last_press_timestamp + SINGLE_PRESS_INTERVAL > release_timestamp && release_pos == press_pos) // 是双击(判断两次单击的间隔) 1132 | { 1133 | double_prevent = true; // 阻止本次的release识别为单击 1134 | press_timestamp = 0; // 避免很可能出现的三击、四击... 1135 | double_timer->stop(); // 取消延迟一小会儿的单击信号 1136 | emit doubleClicked(); 1137 | return; 1138 | } 1139 | else 1140 | { 1141 | double_prevent = false; // 避免有额外的 bug 1142 | } 1143 | } 1144 | else 1145 | { 1146 | press_timestamp = getTimestamp(); 1147 | } 1148 | 1149 | if (water_animation) 1150 | { 1151 | if (waters.size() && waters.last().release_timestamp == 0) // 避免两个按键同时按下 1152 | waters.last().release_timestamp = getTimestamp(); 1153 | waters << Water(press_pos, press_timestamp); 1154 | } 1155 | else // 透明渐变 1156 | { 1157 | if (press_progress < press_start) 1158 | press_progress = press_start; // 直接设置为按下效果初始值(避免按下反应慢) 1159 | } 1160 | } 1161 | mouse_press_event = event; 1162 | emit signalMousePress(event); 1163 | 1164 | return QPushButton::mousePressEvent(event); 1165 | } 1166 | 1167 | /** 1168 | * 鼠标松开事件,触发 release 时间戳 1169 | * 添加抖动动画 jitters 队列 1170 | */ 1171 | void InteractiveButtonBase::mouseReleaseEvent(QMouseEvent *event) 1172 | { 1173 | if (pressing && event->button() == Qt::LeftButton) 1174 | { 1175 | if (!inArea(event->pos()) || leave_after_clicked) 1176 | { 1177 | hovering = false; 1178 | } 1179 | pressing = false; 1180 | release_pos = event->pos(); 1181 | release_timestamp = getTimestamp(); 1182 | 1183 | // 添加抖动效果 1184 | if (jitter_animation) 1185 | { 1186 | setJitter(); 1187 | } 1188 | 1189 | if (water_animation && waters.size()) 1190 | { 1191 | waters.last().release_timestamp = release_timestamp; 1192 | } 1193 | 1194 | if (double_clicked) 1195 | { 1196 | if (double_prevent) // 双击的当次release,不参与单击计算 1197 | { 1198 | double_prevent = false; 1199 | return; 1200 | } 1201 | 1202 | // 应该不是双击的操作 1203 | if (release_pos != press_pos || release_timestamp - press_timestamp >= SINGLE_PRESS_INTERVAL) 1204 | { 1205 | } 1206 | else // 可能是双击,准备 1207 | { 1208 | double_timer->start(); 1209 | return; // 禁止单击事件 1210 | } 1211 | } 1212 | } 1213 | else if (leave_after_clicked && !pressing && double_clicked && double_prevent) // 双击,失去焦点了,pressing 丢失 1214 | { 1215 | return; 1216 | } 1217 | else if (event->button() == Qt::RightButton && event->buttons() == Qt::NoButton) 1218 | { 1219 | if ((release_pos - press_pos).manhattanLength() < QApplication::startDragDistance()) 1220 | emit rightClicked(); 1221 | } 1222 | mouse_release_event = event; 1223 | emit signalMouseRelease(event); 1224 | 1225 | return QPushButton::mouseReleaseEvent(event); 1226 | } 1227 | 1228 | /** 1229 | * 鼠标移动事件 1230 | */ 1231 | void InteractiveButtonBase::mouseMoveEvent(QMouseEvent *event) 1232 | { 1233 | if (_block_hover) // 临时屏蔽hover事件 1234 | { 1235 | if (event) 1236 | event->accept(); 1237 | return; 1238 | } 1239 | if (hovering == false) // 失去焦点又回来了 1240 | { 1241 | enterEvent(nullptr); 1242 | } 1243 | mouse_pos = mapFromGlobal(QCursor::pos()); 1244 | 1245 | return QPushButton::mouseMoveEvent(event); 1246 | } 1247 | 1248 | /** 1249 | * 尺寸大小改变事件 1250 | * 同步调整和尺寸有关的所有属性 1251 | */ 1252 | void InteractiveButtonBase::resizeEvent(QResizeEvent *event) 1253 | { 1254 | if (!pressing && !hovering) 1255 | { 1256 | mouse_pos = QPoint(width() / 2, height() / 2); 1257 | anchor_pos = mouse_pos; 1258 | } 1259 | water_radius = static_cast(max(width(), height()) * 1.42); // 长边 1260 | // 非固定的情况,尺寸大小变了之后所有 padding 都要变 1261 | if (!fixed_fore_size && (model == PaintModel::Icon || model == PaintModel::PixmapMask)) 1262 | { 1263 | int short_side = min(width(), height()); // 短边 1264 | int padding = int(short_side * icon_padding_proper); //static_cast(short_side * (1 - GOLDEN_RATIO) / 2); 1265 | fore_paddings.left = fore_paddings.top = fore_paddings.right = fore_paddings.bottom = padding; 1266 | } 1267 | _l = _t = 0; 1268 | _w = width(); 1269 | _h = height(); 1270 | 1271 | return QPushButton::resizeEvent(event); 1272 | } 1273 | 1274 | /** 1275 | * 获得焦点事件 1276 | * 已经取消按钮获取焦点,focusIn和focusOut事件都不会触发 1277 | */ 1278 | void InteractiveButtonBase::focusInEvent(QFocusEvent *event) 1279 | { 1280 | if (!hovering && inArea(mapFromGlobal(QCursor::pos()))) 1281 | InteractiveButtonBase::enterEvent(new QEvent(QEvent::Type::None)); 1282 | 1283 | focusing = true; 1284 | emit signalFocusIn(); 1285 | 1286 | return QPushButton::focusInEvent(event); 1287 | } 1288 | 1289 | /** 1290 | * 失去焦点事件 1291 | * 兼容按住时突然失去焦点(例如弹出菜单、被其他窗口抢走了) 1292 | */ 1293 | void InteractiveButtonBase::focusOutEvent(QFocusEvent *event) 1294 | { 1295 | if (hovering) 1296 | { 1297 | hovering = false; 1298 | } 1299 | if (pressing) // 鼠标一直按住,可能在click事件中移动了焦点 1300 | { 1301 | pressing = false; 1302 | release_pos = mapFromGlobal(QCursor::pos()); 1303 | release_timestamp = getTimestamp(); 1304 | 1305 | if (water_animation && waters.size()) 1306 | { 1307 | waters.last().release_timestamp = release_timestamp; 1308 | } 1309 | } 1310 | 1311 | focusing = false; 1312 | emit signalFocusOut(); 1313 | 1314 | return QPushButton::focusOutEvent(event); 1315 | } 1316 | 1317 | /** 1318 | * 重绘事件 1319 | * 绘制所有内容:背景、动画、前景、角标 1320 | */ 1321 | void InteractiveButtonBase::paintEvent(QPaintEvent *event) 1322 | { 1323 | if (parent_enabled) // 绘制父类(以便使用父类的QSS和各项属性) 1324 | QPushButton::paintEvent(event); 1325 | if (!self_enabled) // 不绘制自己 1326 | return; 1327 | QPainter painter(this); 1328 | 1329 | // ==== 绘制背景 ==== 1330 | QPainterPath path_back = getBgPainterPath(); 1331 | painter.setRenderHint(QPainter::Antialiasing, true); 1332 | 1333 | if (normal_bg.alpha() != 0) // 默认背景 1334 | { 1335 | painter.fillPath(path_back, isEnabled() ? normal_bg : getOpacityColor(normal_bg)); 1336 | } 1337 | if (focusing && focus_bg.alpha() != 0) // 焦点背景 1338 | { 1339 | painter.fillPath(path_back, focus_bg); 1340 | } 1341 | 1342 | if ((border_bg.alpha() != 0 || (focusing && focus_border.alpha() != 0)) && border_width > 0) 1343 | { 1344 | painter.save(); 1345 | QPen pen; 1346 | pen.setColor((focusing && focus_border.alpha()) ? focus_border : border_bg); 1347 | pen.setWidth(border_width); 1348 | painter.setPen(pen); 1349 | painter.drawPath(path_back); 1350 | painter.restore(); 1351 | } 1352 | 1353 | if (hover_progress) // 悬浮背景 1354 | { 1355 | painter.fillPath(path_back, getOpacityColor(hover_bg, hover_progress / 100.0)); 1356 | } 1357 | 1358 | if (press_progress && !water_animation) // 按下渐变淡化消失 1359 | { 1360 | painter.fillPath(path_back, getOpacityColor(press_bg, press_progress / 100.0)); 1361 | } 1362 | else if (water_animation && waters.size()) // 水波纹,且至少有一个水波纹 1363 | { 1364 | paintWaterRipple(painter); 1365 | } 1366 | 1367 | // ==== 绘制前景 ==== 1368 | if (fore_enabled /*针对按钮设置*/ && show_foreground /*针对动画设置*/) 1369 | { 1370 | painter.setPen(isEnabled() ? icon_color : getOpacityColor(icon_color)); 1371 | 1372 | // 绘制额外内容(可能被前景覆盖) 1373 | if (paint_addin.enable) 1374 | { 1375 | int l = fore_paddings.left, t = fore_paddings.top, r = width() - fore_paddings.right, b = height() - fore_paddings.bottom; 1376 | int small_edge = min(height(), width()); 1377 | int pw = paint_addin.size.width() ? paint_addin.size.width() : small_edge * 4 / 5; 1378 | int ph = paint_addin.size.height() ? paint_addin.size.height() : small_edge * 4 / 5; 1379 | if (paint_addin.align & Qt::AlignLeft) 1380 | r = l + pw; 1381 | else if (paint_addin.align & Qt::AlignRight) 1382 | l = r - pw; 1383 | else if (paint_addin.align & Qt::AlignHCenter) 1384 | { 1385 | l = width() / 2 - pw / 2; 1386 | r = l + pw; 1387 | } 1388 | if (paint_addin.align & Qt::AlignTop) 1389 | b = t + ph; 1390 | else if (paint_addin.align & Qt::AlignBottom) 1391 | t = b - ph; 1392 | else if (paint_addin.align & Qt::AlignVCenter) 1393 | { 1394 | t = height() / 2 - ph / 2; 1395 | b = t + ph; 1396 | } 1397 | painter.drawPixmap(QRect(l, t, r - l, b - t), paint_addin.pixmap); 1398 | } 1399 | 1400 | QRectF &rect = paint_rect; 1401 | rect = QRectF(fore_paddings.left + (fixed_fore_pos ? 0 : offset_pos.x()), 1402 | fore_paddings.top + (fixed_fore_pos ? 0 : offset_pos.y()), // 原来的位置,不包含点击、出现效果 1403 | width() - fore_paddings.left - fore_paddings.right, 1404 | height() - fore_paddings.top - fore_paddings.bottom); 1405 | 1406 | // 抖动出现动画 1407 | if ((show_ani_appearing || show_ani_disappearing) && show_ani_point != QPoint(0, 0) && !fixed_fore_pos) 1408 | { 1409 | //int w = width(), h = height(); 1410 | int pro = getSpringBackProgress(show_ani_progress, 50); 1411 | 1412 | // show_ani_point 是鼠标进入的点,那么起始方向应该是相反的 1413 | double x = show_ani_point.x(), y = show_ani_point.y(); 1414 | int gen = quick_sqrt(long(x * x + y * y)); 1415 | x = water_radius * x / gen; // 动画起始中心点横坐标 反向 1416 | y = water_radius * y / gen; // 动画起始中心点纵坐标 反向 1417 | 1418 | rect = QRectF( 1419 | rect.left() - x * (100 - pro) / 100 + rect.width() * (100 - pro) / 100, 1420 | rect.top() - y * (100 - pro) / 100 + rect.height() * (100 - pro) / 100, 1421 | rect.width() * pro / 100, 1422 | rect.height() * pro / 100); 1423 | } 1424 | else if (align == Qt::AlignCenter && model != PaintModel::Text && !fixed_fore_size) // 默认的缩放动画 1425 | { 1426 | double delta_x = 0, delta_y = 0; 1427 | if (click_ani_progress != 0) // 图标缩放 1428 | { 1429 | delta_x = rect.width() * click_ani_progress / 400; 1430 | delta_y = rect.height() * click_ani_progress / 400; 1431 | } 1432 | else if (show_ani_appearing) 1433 | { 1434 | /*int pro; // 将动画进度转换为回弹动画进度 1435 | if (show_ani_progress <= 50) 1436 | pro = show_ani_progress * 2; 1437 | else if (show_ani_progress <= 75) 1438 | pro = (show_ani_progress-50)/2 + 100; 1439 | else 1440 | pro = 100 + (100-show_ani_progress)/2; 1441 | 1442 | delta_x = rect.width() * (100-pro) / 100; 1443 | delta_y = rect.height() * (100-pro) / 100;*/ 1444 | 1445 | double pro = getNolinearProg(show_ani_progress, SpringBack50); 1446 | delta_x = static_cast(rect.width() * (1 - pro)); 1447 | delta_y = static_cast(rect.height() * (1 - pro)); 1448 | } 1449 | else if (show_ani_disappearing) 1450 | { 1451 | double pro = 1 - getNolinearProg(show_ani_progress, SlowFaster); 1452 | delta_x = rect.width() * pro; // (100-show_ani_progress) / 100; 1453 | delta_y = rect.height() * pro; // (100-show_ani_progress) / 100; 1454 | } 1455 | if (int(delta_x+1e-6) || int(delta_y+1e-6)) 1456 | rect = QRectF(rect.left() + delta_x, rect.top() + delta_y, 1457 | rect.width() - delta_x * 2, rect.height() - delta_y * 2); 1458 | } 1459 | 1460 | /*if (this->isEnabled()) 1461 | { 1462 | QColor color = icon_color; 1463 | color.setAlpha(color.alpha() / 2); 1464 | painter.setPen(color); 1465 | }*/ 1466 | 1467 | if (model == None) 1468 | { 1469 | // 子类自己的绘制内容 1470 | } 1471 | else if (model == Text) 1472 | { 1473 | // 绘制文字教程: https://blog.csdn.net/temetnosce/article/details/78068464 1474 | painter.setPen(isEnabled() ? text_color : getOpacityColor(text_color)); 1475 | /*if (show_ani_appearing || show_ani_disappearing) 1476 | { 1477 | int pro = getSpringBackProgress(show_ani_progress, 50); 1478 | QFont font = painter.font(); 1479 | int ps = font.pointSize(); 1480 | ps = ps * show_ani_progress / 100; 1481 | font.setPointSize(ps); 1482 | painter.setFont(font); 1483 | }*/ 1484 | if (font_size > 0) 1485 | { 1486 | QFont font = painter.font(); 1487 | font.setPointSize(font_size); 1488 | painter.setFont(font); 1489 | } 1490 | painter.drawText(rect, static_cast(align), text); 1491 | } 1492 | else if (model == Icon) // 绘制图标 1493 | { 1494 | icon.paint(&painter, rect.toRect(), align, getIconMode()); 1495 | } 1496 | else if (model == PixmapMask) 1497 | { 1498 | painter.setRenderHint(QPainter::SmoothPixmapTransform, true); // 可以让边缘看起来平滑一些 1499 | painter.drawPixmap(rect.toRect(), pixmap); 1500 | } 1501 | else if (model == IconText || model == PixmapText) // 强制左对齐;左图标中文字 1502 | { 1503 | // 绘制图标 1504 | int &sz = icon_text_size; 1505 | QRectF icon_rect(rect.left(), rect.top() + rect.height() / 2 - sz / 2, sz, sz); 1506 | icon_rect.moveTo(icon_rect.left() - quick_sqrt(long(offset_pos.x())), icon_rect.top() - quick_sqrt(long(offset_pos.y()))); 1507 | drawIconBeforeText(painter, icon_rect.toRect()); 1508 | rect.setLeft(rect.left() + sz + icon_text_padding); 1509 | 1510 | // 绘制文字 1511 | // 扩展文字范围,确保文字可见 1512 | painter.setPen(isEnabled() ? text_color : getOpacityColor(text_color)); 1513 | rect.setWidth(rect.width() + sz + icon_text_padding); 1514 | if (font_size > 0) 1515 | { 1516 | QFont font = painter.font(); 1517 | font.setPointSize(font_size); 1518 | painter.setFont(font); 1519 | } 1520 | painter.drawText(rect, Qt::AlignLeft | Qt::AlignVCenter, text); 1521 | } 1522 | } 1523 | 1524 | // ==== 绘制鼠标位置 ==== 1525 | // painter.drawEllipse(QRect(anchor_pos.x()-5, anchor_pos.y()-5, 10, 10)); // 移动锚点 1526 | // painter.drawEllipse(QRect(effect_pos.x()-2, effect_pos.y()-2, 4, 4)); // 影响位置锚点 1527 | 1528 | // return QPushButton::paintEvent(event); // 不绘制父类背景了 1529 | } 1530 | 1531 | /** 1532 | * IconText/PixmapText模式下,绘制图标 1533 | * 可扩展到绘制图标背景色(模仿menu选中、禁用情况)等 1534 | */ 1535 | void InteractiveButtonBase::drawIconBeforeText(QPainter &painter, QRect icon_rect) 1536 | { 1537 | if (model == IconText) 1538 | icon.paint(&painter, icon_rect, align, getIconMode()); 1539 | else if (model == PixmapText) 1540 | painter.drawPixmap(icon_rect, pixmap); 1541 | } 1542 | 1543 | /** 1544 | * 判断坐标是否在按钮区域内 1545 | * 避免失去了焦点,但是依旧需要 hover 效果(非菜单和弹窗抢走焦点) 1546 | * 为子类异形按钮区域判断提供支持 1547 | * @param point 当前鼠标 1548 | * @return 是否在区域内 1549 | */ 1550 | bool InteractiveButtonBase::inArea(QPoint point) 1551 | { 1552 | return !(point.x() < 0 || point.y() < 0 || point.x() > width() || point.y() > height()); 1553 | } 1554 | 1555 | bool InteractiveButtonBase::inArea(QPointF point) 1556 | { 1557 | return !(point.x() < 0 || point.y() < 0 || point.x() > width() || point.y() > height()); 1558 | } 1559 | 1560 | /** 1561 | * 获取按钮背景的绘制区域 1562 | * 为子类异形按钮提供支持 1563 | * @return [description] 1564 | */ 1565 | QPainterPath InteractiveButtonBase::getBgPainterPath() 1566 | { 1567 | QPainterPath path; 1568 | if (radius_x || radius_y) 1569 | path.addRoundedRect(QRect(0, 0, width(), height()), radius_x, radius_y); 1570 | else 1571 | path.addRect(QRect(0, 0, width(), height())); 1572 | return path; 1573 | } 1574 | 1575 | /** 1576 | * 获取水波纹绘制区域(圆形,但不规则区域) 1577 | * 圆形水面 & 按钮区域 1578 | * @param water 一面水波纹动画对象 1579 | * @return 绘制路径 1580 | */ 1581 | QPainterPath InteractiveButtonBase::getWaterPainterPath(InteractiveButtonBase::Water water) 1582 | { 1583 | double prog = getNolinearProg(water.progress, FastSlower); 1584 | double ra = water_radius * prog; 1585 | QRectF circle(water.point.x() - ra, 1586 | water.point.y() - ra, 1587 | ra * 2, 1588 | ra * 2); 1589 | /*QRect circle(water.point.x() - water_radius*water.progress/100, 1590 | water.point.y() - water_radius*water.progress/100, 1591 | water_radius*water.progress/50, 1592 | water_radius*water.progress/50);*/ 1593 | QPainterPath path; 1594 | path.addEllipse(circle); 1595 | if (radius_x || radius_y) 1596 | return path & getBgPainterPath(); 1597 | return path; 1598 | } 1599 | 1600 | /** 1601 | * 获取统一的尺寸大小(已废弃) 1602 | * 兼容圆形按钮出现动画,半径使用水波纹(对角线) 1603 | * 可直接使用 protected 对象 1604 | * @return 前景绘制区域 1605 | */ 1606 | QRectF InteractiveButtonBase::getUnifiedGeometry() 1607 | { 1608 | // 将动画进度转换为回弹动画进度 1609 | int pro = show_ani_appearing ? getSpringBackProgress(show_ani_progress, 50) : show_ani_progress; 1610 | double ul = 0, ut = 0, uw = width(), uh = height(); 1611 | 1612 | // show_ani_point 是鼠标进入的点,那么起始方向应该是相反的 1613 | double x = show_ani_point.x(), y = show_ani_point.y(); 1614 | int gen = quick_sqrt(long(x * x + y * y)); 1615 | x = -water_radius * x / gen; // 动画起始中心点横坐标 反向 1616 | y = -water_radius * y / gen; // 动画起始中心点纵坐标 反向 1617 | 1618 | ul = ul + x * (100 - pro) / 100 + uw * (100 - pro) / 200; 1619 | ut = ut + y * (100 - pro) / 100 + uh * (100 - pro) / 200; 1620 | uw = uw * pro / 100; 1621 | uh = uh * pro / 100; 1622 | 1623 | return QRectF(ul, ut, uw, uh); 1624 | } 1625 | 1626 | /** 1627 | * 更新统一绘制区域 1628 | * 内部的 _l, _t, _w, _h 可直接使用 1629 | */ 1630 | void InteractiveButtonBase::updateUnifiedGeometry() 1631 | { 1632 | _l = 0; 1633 | _t = 0; 1634 | _w = width(); 1635 | _h = height(); 1636 | if ((show_ani_appearing || show_ani_disappearing) && show_ani_point != QPoint(0, 0)) 1637 | { 1638 | int pro; // 将动画进度转换为回弹动画进度 1639 | pro = show_ani_appearing ? getSpringBackProgress(show_ani_progress, 50) : show_ani_progress; 1640 | 1641 | // show_ani_point 是鼠标进入的点,那么起始方向应该是相反的 1642 | double x = show_ani_point.x(), y = show_ani_point.y(); 1643 | int gen = quick_sqrt(long(x * x + y * y)); 1644 | x = -water_radius * x / gen; // 动画起始中心点横坐标 反向 1645 | y = -water_radius * y / gen; // 动画起始中心点纵坐标 反向 1646 | 1647 | _l = _l + x * (100 - pro) / 100 + _w * (100 - pro) / 200; 1648 | _t = _t + y * (100 - pro) / 100 + _h * (100 - pro) / 200; 1649 | _w = _w * pro / 100; 1650 | _h = _h * pro / 100; 1651 | } 1652 | } 1653 | 1654 | /** 1655 | * 绘制一个水波纹动画 1656 | * @param painter 绘制对象(即painter(this)对象) 1657 | */ 1658 | void InteractiveButtonBase::paintWaterRipple(QPainter &painter) 1659 | { 1660 | QColor water_finished_color(press_bg); 1661 | 1662 | for (int i = 0; i < waters.size(); i++) 1663 | { 1664 | Water water = waters.at(i); 1665 | if (water.finished) // 渐变消失 1666 | { 1667 | water_finished_color.setAlpha(press_bg.alpha() * water.progress / 100); 1668 | QPainterPath path_back = getBgPainterPath(); 1669 | // painter.setPen(water_finished_color); 1670 | painter.fillPath(path_back, QBrush(water_finished_color)); 1671 | } 1672 | else // 圆形出现 1673 | { 1674 | QPainterPath path = getWaterPainterPath(water); 1675 | painter.fillPath(path, QBrush(press_bg)); 1676 | } 1677 | } 1678 | } 1679 | 1680 | /** 1681 | * 鼠标松开的时候,计算所有抖动效果的路径和事件 1682 | * 在每次重绘界面的时候,依次遍历所有的路径 1683 | */ 1684 | void InteractiveButtonBase::setJitter() 1685 | { 1686 | jitters.clear(); 1687 | QPoint center_pos = geometry().center() - geometry().topLeft(); 1688 | double full_manh = (anchor_pos - center_pos).manhattanLength(); // 距离 1689 | // 是否达到需要抖动的距离 1690 | if (full_manh > (geometry().topLeft() - geometry().bottomRight()).manhattanLength()) // 距离超过外接圆半径,开启抖动 1691 | { 1692 | QPointF jitter_pos(effect_pos); 1693 | full_manh = (jitter_pos - center_pos).manhattanLength(); 1694 | double manh = full_manh; 1695 | int duration = jitter_duration; 1696 | qint64 timestamp = release_timestamp; 1697 | while (manh > elastic_coefficient) 1698 | { 1699 | jitters << Jitter(jitter_pos, timestamp); 1700 | jitter_pos = center_pos - (jitter_pos - center_pos) / elastic_coefficient; 1701 | duration = int(jitter_duration * manh / full_manh); 1702 | timestamp += duration; 1703 | manh = static_cast(manh / elastic_coefficient); 1704 | } 1705 | jitters << Jitter(center_pos, timestamp); 1706 | anchor_pos = mouse_pos = center_pos; 1707 | } 1708 | else if (!hovering) // 悬浮的时候依旧有效 1709 | { 1710 | // 未达到抖动距离,直接恢复 1711 | mouse_pos = center_pos; 1712 | } 1713 | } 1714 | 1715 | /** 1716 | * 速度极快的开方算法,效率未知,原理未知 1717 | * @param X 待开方的数字 1718 | * @return 平方根 1719 | */ 1720 | int InteractiveButtonBase::quick_sqrt(long X) const 1721 | { 1722 | bool fu = false; 1723 | if (X < 0) 1724 | { 1725 | fu = true; 1726 | X = -X; 1727 | } 1728 | #if !defined(Q_OS_WIN) 1729 | X = qSqrt(X); 1730 | return fu ? -X : X; 1731 | #endif 1732 | unsigned long M = static_cast(X); 1733 | unsigned int N, i; 1734 | unsigned long tmp, ttp; // 结果、循环计数 1735 | if (M == 0) // 被开方数,开方结果也为0 1736 | return 0; 1737 | N = 0; 1738 | tmp = (M >> 30); // 获取最高位:B[m-1] 1739 | M <<= 2; 1740 | if (tmp > 1) // 最高位为1 1741 | { 1742 | N++; // 结果当前位为1,否则为默认的0 1743 | tmp -= N; 1744 | } 1745 | for (i = 15; i > 0; i--) // 求剩余的15位 1746 | { 1747 | N <<= 1; // 左移一位 1748 | tmp <<= 2; 1749 | tmp += (M >> 30); // 假设 1750 | ttp = N; 1751 | ttp = (ttp << 1) + 1; 1752 | M <<= 2; 1753 | if (tmp >= ttp) // 假设成立 1754 | { 1755 | tmp -= ttp; 1756 | N++; 1757 | } 1758 | } 1759 | return (fu ? -1 : 1) * static_cast(N); // 不知道为什么计算出来的结果是反过来的 1760 | } 1761 | 1762 | /** 1763 | * 最大值 1764 | */ 1765 | int InteractiveButtonBase::max(int a, int b) const { return a > b ? a : b; } 1766 | 1767 | /** 1768 | * 最小值 1769 | */ 1770 | int InteractiveButtonBase::min(int a, int b) const { return a < b ? a : b; } 1771 | 1772 | /** 1773 | * 获取现行时间戳,13位,精确到毫秒 1774 | * @return 时间戳 1775 | */ 1776 | qint64 InteractiveButtonBase::getTimestamp() const 1777 | { 1778 | return QDateTime::currentDateTime().toMSecsSinceEpoch(); 1779 | } 1780 | 1781 | /** 1782 | * 是否为亮色颜色 1783 | * @param color 颜色 1784 | * @return 是否为亮色 1785 | */ 1786 | bool InteractiveButtonBase::isLightColor(QColor color) 1787 | { 1788 | return color.red() * 0.299 + color.green() * 0.578 + color.blue() * 0.114 >= 192; 1789 | } 1790 | 1791 | /** 1792 | * 获取非线性动画在某一时间比例的动画进度 1793 | * 仅适用于弹过头效果的动画 1794 | * @param x 实际相对完整100%的动画进度 1795 | * @param max 前半部分动画进度上限 1796 | * @return 应当显示的动画进度 1797 | */ 1798 | int InteractiveButtonBase::getSpringBackProgress(int x, int max) 1799 | { 1800 | if (x <= max) 1801 | return x * 100 / max; 1802 | if (x <= max + (100 - max) / 2) 1803 | return (x - max) / 2 + 100; 1804 | return 100 + (100 - x) / 2; 1805 | } 1806 | 1807 | /** 1808 | * 获取透明的颜色 1809 | * @param color 颜色 1810 | * @param level 比例 1811 | * @return 透明颜色 1812 | */ 1813 | QColor InteractiveButtonBase::getOpacityColor(QColor color, double level) 1814 | { 1815 | color.setAlpha(static_cast(color.alpha() * level)); 1816 | return color; 1817 | } 1818 | 1819 | /** 1820 | * 获取对应颜色的图标 pixmap 1821 | * @param p 图标 1822 | * @param c 颜色 1823 | * @return 对应颜色的图标 1824 | */ 1825 | QPixmap InteractiveButtonBase::getMaskPixmap(QPixmap p, QColor c) 1826 | { 1827 | QBitmap mask = p.mask(); 1828 | p.fill(c); 1829 | p.setMask(mask); 1830 | return p; 1831 | } 1832 | 1833 | double InteractiveButtonBase::getNolinearProg(int p, InteractiveButtonBase::NolinearType type) 1834 | { 1835 | if (p <= 0) 1836 | return 0.0; 1837 | if (p >= 100) 1838 | return 1.0; 1839 | 1840 | switch (type) 1841 | { 1842 | case Linear: 1843 | return p / 100.0; 1844 | case SlowFaster: 1845 | return p * p / 10000.0; 1846 | case FastSlower: 1847 | return quick_sqrt(p * 100) / 100.0; 1848 | case SlowFastSlower: 1849 | if (p <= 50) 1850 | return p * p / 50.0; 1851 | else 1852 | return 0.5 + quick_sqrt(50 * (p - 50)) / 100.0; 1853 | case SpringBack20: 1854 | case SpringBack50: 1855 | if (p <= 50) 1856 | return p / 50.0; 1857 | else if (p < 75) 1858 | return 1.0 + (p - 50) / 200.0; 1859 | else 1860 | return 1.0 + (100 - p) / 200.0; 1861 | } 1862 | } 1863 | 1864 | QIcon::Mode InteractiveButtonBase::getIconMode() 1865 | { 1866 | return isEnabled() ? (getState() ? QIcon::Selected : (hovering || pressing ? QIcon::Active : QIcon::Normal)) : QIcon::Disabled; 1867 | } 1868 | 1869 | /** 1870 | * 锚点变成到鼠标位置的定时时钟 1871 | * 同步计算所有和时间或者帧数有关的动画和属性 1872 | */ 1873 | void InteractiveButtonBase::anchorTimeOut() 1874 | { 1875 | qint64 timestamp = getTimestamp(); 1876 | // ==== 背景色 ==== 1877 | /*if (hovering) // 在框内:加深 1878 | { 1879 | if (hover_progress < 100) // 先判断,再计算,可节约运算资源 1880 | hover_progress = min((timestamp - hover_timestamp) * 100 / press_bg_duration, 100); 1881 | } 1882 | else // 在框外:变浅 1883 | { 1884 | if (hover_progress > 0) 1885 | hover_progress = max((timestamp - leave_timestamp) * 100 / press_bg_duration, 0); 1886 | } 1887 | 1888 | if (pressing) 1889 | { 1890 | if (press_progress < 100) 1891 | press_progress = min(press_start + (timestamp - press_timestamp) * 100 / press_bg_duration, 100); 1892 | } 1893 | else 1894 | { 1895 | if (press_progress > 0) // 如果按下的效果还在,变浅 1896 | press_progress = max((timestamp - release_timestamp) * 100 / press_bg_duration, 0); 1897 | }*/ 1898 | 1899 | if (pressing) // 鼠标按下 1900 | { 1901 | if (press_progress < 100) // 透明渐变,且没有完成 1902 | { 1903 | press_progress += press_speed; 1904 | if (press_progress >= 100) 1905 | { 1906 | press_progress = 100; 1907 | if (mouse_press_event) 1908 | { 1909 | emit signalMousePressLater(mouse_press_event); 1910 | mouse_press_event = nullptr; 1911 | } 1912 | } 1913 | } 1914 | if (hovering && hover_progress < 100) 1915 | { 1916 | hover_progress += hover_speed; 1917 | if (hover_progress >= 100) 1918 | { 1919 | hover_progress = 100; 1920 | emit signalMouseEnterLater(); 1921 | } 1922 | } 1923 | } 1924 | else // 鼠标悬浮 1925 | { 1926 | if (press_progress > 0) // 如果按下的效果还在,变浅 1927 | { 1928 | press_progress -= press_speed; 1929 | if (press_progress <= 0) 1930 | { 1931 | press_progress = 0; 1932 | if (mouse_release_event) 1933 | { 1934 | emit signalMouseReleaseLater(mouse_release_event); 1935 | mouse_release_event = nullptr; 1936 | } 1937 | } 1938 | } 1939 | 1940 | if (hovering) // 在框内:加深 1941 | { 1942 | if (hover_progress < 100) 1943 | { 1944 | hover_progress += hover_speed; 1945 | if (hover_progress >= 100) 1946 | { 1947 | hover_progress = 100; 1948 | emit signalMouseEnterLater(); 1949 | } 1950 | } 1951 | } 1952 | else // 在框外:变浅 1953 | { 1954 | if (hover_progress > 0) 1955 | { 1956 | hover_progress -= hover_speed; 1957 | if (hover_progress <= 0) 1958 | { 1959 | hover_progress = 0; 1960 | emit signalMouseLeaveLater(); 1961 | } 1962 | } 1963 | } 1964 | } 1965 | 1966 | // ==== 按下背景水波纹动画 ==== 1967 | if (water_animation) 1968 | { 1969 | for (int i = 0; i < waters.size(); i++) 1970 | { 1971 | Water &water = waters[i]; 1972 | if (water.finished) // 结束状态 1973 | { 1974 | water.progress = static_cast(100 - 100 * (timestamp - water.finish_timestamp) / water_finish_duration); 1975 | if (water.progress <= 0) 1976 | { 1977 | waters.removeAt(i--); 1978 | if (mouse_release_event) // 还没有发送按下延迟信号 1979 | { 1980 | emit signalMouseReleaseLater(mouse_release_event); 1981 | mouse_release_event = nullptr; 1982 | } 1983 | } 1984 | } 1985 | else // 正在出现状态 1986 | { 1987 | if (water.progress >= 100) // 满了 1988 | { 1989 | water.progress = 100; 1990 | if (water.release_timestamp) // 鼠标已经松开了 1991 | { 1992 | water.finished = true; // 准备结束 1993 | water.finish_timestamp = timestamp; 1994 | } 1995 | } 1996 | else // 动画中的 1997 | { 1998 | if (water.release_timestamp) // 鼠标已经松开了 1999 | { 2000 | water.progress = static_cast(100 * (water.release_timestamp - water.press_timestamp) / water_press_duration + 100 * (timestamp - water.release_timestamp) / water_release_duration); 2001 | } 2002 | else // 鼠标一直按下 2003 | { 2004 | water.progress = static_cast(100 * (timestamp - water.press_timestamp) / water_press_duration); 2005 | } 2006 | if (water.progress >= 100) 2007 | { 2008 | water.progress = 100; 2009 | if (mouse_press_event) // 还没有发送按下延迟信号 2010 | { 2011 | emit signalMousePressLater(mouse_press_event); 2012 | mouse_press_event = nullptr; 2013 | } 2014 | } 2015 | } 2016 | } 2017 | } 2018 | } 2019 | 2020 | // ==== 出现动画 ==== 2021 | if (show_animation) 2022 | { 2023 | if (show_ani_appearing) // 出现 2024 | { 2025 | qint64 delta = getTimestamp() - show_timestamp; 2026 | if (show_ani_progress >= 100) // 出现结束 2027 | { 2028 | show_ani_appearing = false; 2029 | emit showAniFinished(); 2030 | } 2031 | else 2032 | { 2033 | show_ani_progress = static_cast(100 * delta / show_duration); 2034 | if (show_ani_progress > 100) 2035 | show_ani_progress = 100; 2036 | } 2037 | } 2038 | if (show_ani_disappearing) // 消失 2039 | { 2040 | qint64 delta = getTimestamp() - hide_timestamp; 2041 | if (show_ani_progress <= 0) // 消失结束 2042 | { 2043 | show_ani_disappearing = false; 2044 | show_foreground = false; 2045 | show_ani_point = QPoint(0, 0); 2046 | emit hideAniFinished(); 2047 | } 2048 | else 2049 | { 2050 | show_ani_progress = static_cast(100 - 100 * delta / show_duration); 2051 | if (show_ani_progress < 0) 2052 | show_ani_progress = 0; 2053 | } 2054 | } 2055 | } 2056 | 2057 | // ==== 按下动画 ==== 2058 | if (click_ani_disappearing) // 点击动画效果消失 2059 | { 2060 | qint64 delta = getTimestamp() - release_timestamp - click_ani_duration; 2061 | if (delta <= 0) 2062 | click_ani_progress = 100; 2063 | else 2064 | click_ani_progress = static_cast(100 - delta * 100 / click_ani_duration); 2065 | if (click_ani_progress < 0) 2066 | { 2067 | click_ani_progress = 0; 2068 | click_ani_disappearing = false; 2069 | emit pressAppearAniFinished(); 2070 | } 2071 | } 2072 | if (click_ani_appearing) // 点击动画效果 2073 | { 2074 | qint64 delta = getTimestamp() - release_timestamp; 2075 | if (delta <= 0) 2076 | click_ani_progress = 0; 2077 | else 2078 | click_ani_progress = static_cast(delta * 100 / click_ani_duration); 2079 | if (click_ani_progress > 100) 2080 | { 2081 | click_ani_progress = 100; // 保持100的状态,下次点击时回到0 2082 | click_ani_appearing = false; 2083 | click_ani_disappearing = true; 2084 | emit pressDisappearAniFinished(); 2085 | } 2086 | } 2087 | 2088 | // ==== 锚点移动 ==== 2089 | if (jitters.size() > 0) // 松开时的抖动效果 2090 | { 2091 | // 当前应该是处在最后一个点 2092 | Jitter cur = jitters.first(); 2093 | Jitter aim = jitters.at(1); 2094 | int del = static_cast(getTimestamp() - cur.timestamp); 2095 | int dur = static_cast(aim.timestamp - cur.timestamp); 2096 | effect_pos = cur.point + (aim.point - cur.point) * del / dur; 2097 | offset_pos = effect_pos - (geometry().center() - geometry().topLeft()); 2098 | 2099 | if (del >= dur) 2100 | jitters.removeFirst(); 2101 | 2102 | // 抖动结束 2103 | if (jitters.size() == 1) 2104 | { 2105 | jitters.clear(); 2106 | emit jitterAniFinished(); 2107 | } 2108 | } 2109 | else if (anchor_pos != mouse_pos) // 移动效果 2110 | { 2111 | double delta_x = anchor_pos.x() - mouse_pos.x(), 2112 | delta_y = anchor_pos.y() - mouse_pos.y(); 2113 | 2114 | anchor_pos.setX(anchor_pos.x() - quick_sqrt(int(delta_x))); 2115 | anchor_pos.setY(anchor_pos.y() - quick_sqrt(int(delta_y))); 2116 | 2117 | offset_pos.setX(quick_sqrt(long(anchor_pos.x() - (width() >> 1)))); 2118 | offset_pos.setY(quick_sqrt(long(anchor_pos.y() - (height() >> 1)))); 2119 | effect_pos.setX((width() >> 1) + offset_pos.x()); 2120 | effect_pos.setY((height() >> 1) + offset_pos.y()); 2121 | } 2122 | else if (!pressing && !hovering && !hover_progress && !press_progress && !click_ani_appearing && !click_ani_disappearing && !jitters.size() && !waters.size() && !show_ani_appearing && !show_ani_disappearing) // 没有需要加载的项,暂停(节约资源) 2123 | { 2124 | anchor_timer->stop(); 2125 | } 2126 | 2127 | // ==== 统一坐标的出现动画 ==== 2128 | if (unified_geometry) 2129 | { 2130 | updateUnifiedGeometry(); 2131 | } 2132 | 2133 | update(); 2134 | } 2135 | 2136 | /** 2137 | * 鼠标单击事件 2138 | * 实测按下后,在按钮区域弹起,不管移动多少距离都算是 clicked 2139 | */ 2140 | void InteractiveButtonBase::slotClicked() 2141 | { 2142 | click_ani_appearing = true; 2143 | click_ani_disappearing = false; 2144 | click_ani_progress = 0; 2145 | release_offset = offset_pos; 2146 | 2147 | jitters.clear(); // 清除抖动 2148 | } 2149 | 2150 | /** 2151 | * 强行关闭状态 2152 | * 以槽的形式,便与利用 2153 | */ 2154 | void InteractiveButtonBase::slotCloseState() 2155 | { 2156 | setState(false); 2157 | } 2158 | -------------------------------------------------------------------------------- /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 | #include 17 | 18 | #define PI 3.1415926 19 | #define GOLDEN_RATIO 0.618 20 | 21 | #define DOUBLE_PRESS_INTERVAL 500 // /* 300 */松开和按下的间隔。相等为双击 22 | #define SINGLE_PRESS_INTERVAL 200 // /* 150 */按下时间超过这个数就是单击。相等为单击 23 | 24 | /** 25 | * Copyright (c) 2019 命燃芯乂 All rights reserved. 26 | × 27 | * 邮箱:wxy@iwxyi.com 28 | * QQ号:482582886 29 | * 时间:2020.12.28 30 | * 31 | * 说明:灵性的自定义按钮,简单又有趣 32 | * 源码:https://github.com/MRXY001/Interactive-Windows-Buttons 33 | * 34 | * 本代码为本人编写方便自己使用,现在无私送给大家免费使用。 35 | * 程序版权归作者所有,只可使用不能出售,违反者本人有权追究责任。 36 | */ 37 | 38 | class InteractiveButtonBase : public QPushButton 39 | { 40 | Q_OBJECT 41 | Q_PROPERTY(bool self_enabled READ getSelfEnabled WRITE setSelfEnabled) // 是否启用自定义的按钮(true) 42 | Q_PROPERTY(bool parent_enabled READ getParentEnabled WRITE setParentEnabled) // 是否启用父类按钮(false) 43 | Q_PROPERTY(bool fore_enabled READ getForeEnabled WRITE setForeEnabled) // 是否绘制自定义按钮前景色(true) 44 | Q_PROPERTY(QString text READ getText WRITE setText) // 前景文字 45 | Q_PROPERTY(QString icon_path READ getIconPath WRITE setIconPath) // 前景图标 46 | Q_PROPERTY(QString pixmap_path READ getPixmapPath WRITE setPixmapPath) // 前景图标 47 | Q_PROPERTY(QColor icon_color READ getIconColor WRITE setIconColor) // 前景图标帅色 48 | Q_PROPERTY(QColor text_color READ getTextColor WRITE setTextColor) // 前景文字颜色 49 | Q_PROPERTY(QColor background_color READ getNormalColor WRITE setNormalColor) // 背景颜色 50 | Q_PROPERTY(QColor border_color READ getBorderColor WRITE setBorderColor) // 边界颜色 51 | Q_PROPERTY(QColor hover_color READ getHoverColor WRITE setHoverColor) // 鼠标悬浮背景颜色 52 | Q_PROPERTY(QColor press_color READ getPressColor WRITE setPressColor) // 鼠标按下背景颜色 53 | Q_PROPERTY(int hover_duration READ getHoverAniDuration WRITE setHoverAniDuration) // 鼠标悬浮动画周期 54 | Q_PROPERTY(int press_duration READ getPressAniDuration WRITE setPressAniDuration) // 鼠标按下动画周期 55 | Q_PROPERTY(int click_duration READ getClickAniDuration WRITE setClickAniDuration) // 鼠标点击动画周期 56 | Q_PROPERTY(double icon_padding_proper READ getIconPaddingProper WRITE setIconPaddingProper) // 图标四边空白处大小比例 57 | Q_PROPERTY(int radius READ getRadius WRITE setRadius) // 边框圆角半径 58 | Q_PROPERTY(int border_width READ getBorderWidth WRITE setBorderWidth) // 边框线条粗细 59 | Q_PROPERTY(bool fixed_fore_pos READ getFixedTextPos WRITE setFixedTextPos) // 是否固定前景位置(false) 60 | Q_PROPERTY(bool text_dynamic_size READ getTextDynamicSize WRITE setTextDynamicSize) // 修改字体大小时调整按钮最小尺寸(false) 61 | Q_PROPERTY(bool leave_after_clicked READ getLeaveAfterClick WRITE setLeaveAfterClick) // 鼠标单击松开后取消悬浮效果(针对菜单、弹窗) 62 | Q_PROPERTY(bool show_animation READ getShowAni WRITE setShowAni) // 是否启用出现动画(鼠标移开则消失)(false) 63 | Q_PROPERTY(bool water_animation READ getWaterRipple WRITE setWaterRipple) // 是否启用点击水波纹动画(否则使用渐变)(true) 64 | Q_PROPERTY(int font_size READ getFontSizeT WRITE setFontSizeT) // 动:按钮字体动画效果(自动,不应该设置) 65 | public: 66 | InteractiveButtonBase(QWidget *parent = nullptr); 67 | InteractiveButtonBase(QString text, QWidget *parent = nullptr); 68 | InteractiveButtonBase(QIcon icon, QWidget *parent = nullptr); 69 | InteractiveButtonBase(QPixmap pixmap, QWidget *parent = nullptr); 70 | InteractiveButtonBase(QIcon icon, QString text, QWidget *parent = nullptr); 71 | InteractiveButtonBase(QPixmap pixmap, QString text, QWidget *parent = nullptr); 72 | 73 | /** 74 | * 前景实体 75 | */ 76 | enum PaintModel 77 | { 78 | None, // 无前景,仅使用背景 79 | Text, // 纯文字(替代父类) 80 | Icon, // 纯图标 81 | PixmapMask, // 可变色图标(通过pixmap+遮罩实现),锯齿化明显 82 | IconText, // 图标+文字(强制左对齐) 83 | PixmapText // 变色图标+文字(强制左对齐) 84 | }; 85 | 86 | /** 87 | * 前景额外的图标(可以多个) 88 | * 可能是角标(比如展开箭头) 89 | * 可能是前缀(图例) 90 | */ 91 | struct PaintAddin 92 | { 93 | PaintAddin() : enable(false) {} 94 | PaintAddin(QPixmap p, Qt::Alignment a, QSize s) : enable(true), pixmap(p), align(a), size(s) {} 95 | bool enable; // 是否启用 96 | QPixmap pixmap; // 可变色图标 97 | Qt::Alignment align; // 对齐方式 98 | QSize size; // 固定大小 99 | }; 100 | 101 | /** 102 | * 鼠标松开时抖动动画 103 | * 松开的时候计算每一次抖动距离+时间,放入队列中 104 | * 定时调整抖动的队列实体索引 105 | */ 106 | struct Jitter 107 | { 108 | Jitter(QPointF p, qint64 t) : point(p), timestamp(t) {} 109 | QPointF point; // 要运动到的目标坐标 110 | qint64 timestamp; // 运动到目标坐标应该的时间戳,结束后删除本次抖动路径对象 111 | }; 112 | 113 | /** 114 | * 鼠标按下/弹起水波纹动画 115 | * 鼠标按下时动画速度慢(压住),松开后动画速度骤然加快 116 | * 同样用队列记录所有的水波纹动画实体 117 | */ 118 | struct Water 119 | { 120 | Water(QPointF p, qint64 t) : point(p), progress(0), press_timestamp(t), 121 | release_timestamp(0), finish_timestamp(0), finished(false) {} 122 | QPointF point; 123 | int progress; // 水波纹进度100%(已弃用,当前使用时间戳) 124 | qint64 press_timestamp; // 鼠标按下时间戳 125 | qint64 release_timestamp; // 鼠标松开时间戳。与按下时间戳、现行时间戳一起成为水波纹进度计算参数 126 | qint64 finish_timestamp; // 结束时间戳。与当前时间戳相减则为渐变消失经过的时间戳 127 | bool finished; // 是否结束。结束后改为渐变消失 128 | }; 129 | 130 | /** 131 | * 四周边界的padding 132 | * 调整按钮大小时:宽度+左右、高度+上下 133 | */ 134 | struct EdgeVal 135 | { 136 | EdgeVal() {} 137 | EdgeVal(int l, int t, int r, int b) : left(l), top(t), right(r), bottom(b) {} 138 | int left, top, right, bottom; // 四个边界的空白距离 139 | }; 140 | 141 | enum NolinearType 142 | { 143 | Linear, 144 | SlowFaster, 145 | FastSlower, 146 | SlowFastSlower, 147 | SpringBack20, 148 | SpringBack50 149 | }; 150 | 151 | virtual void setText(QString text); 152 | virtual void setIconPath(QString path); 153 | virtual void setIcon(QIcon icon); 154 | virtual void setPixmapPath(QString path); 155 | virtual void setPixmap(QPixmap pixmap); 156 | virtual void setPaintAddin(QPixmap pixmap, Qt::Alignment align = Qt::AlignRight, QSize size = QSize(0, 0)); 157 | 158 | void setSelfEnabled(bool e = true); 159 | void setParentEnabled(bool e = false); 160 | void setForeEnabled(bool e = true); 161 | 162 | void setHoverAniDuration(int d); 163 | void setPressAniDuration(int d); 164 | void setClickAniDuration(int d); 165 | void setWaterAniDuration(int press, int release, int finish); 166 | void setWaterRipple(bool enable = true); 167 | void setJitterAni(bool enable = true); 168 | void setUnifyGeomerey(bool enable = true); 169 | void setBgColor(QColor bg); 170 | void setBgColor(QColor hover, QColor press); 171 | void setNormalColor(QColor color); 172 | void setBorderColor(QColor color); 173 | void setHoverColor(QColor color); 174 | void setPressColor(QColor color); 175 | void setIconColor(QColor color = QColor(0, 0, 0)); 176 | void setTextColor(QColor color = QColor(0, 0, 0)); 177 | void setFocusBg(QColor color); 178 | void setFocusBorder(QColor color); 179 | void setFontSize(int f); 180 | void setHover(); 181 | void setAlign(Qt::Alignment a); 182 | void setRadius(int r); 183 | void setRadius(int rx, int ry); 184 | void setBorderWidth(int x); 185 | void setDisabled(bool dis = true); 186 | void setPaddings(int l, int r, int t, int b); 187 | void setPaddings(int h, int v); 188 | void setPaddings(int x); 189 | void setIconPaddingProper(double x); 190 | void setFixedForePos(bool f = true); 191 | void setFixedForeSize(bool f = true, int addin = 0); 192 | void setSquareSize(); 193 | void setTextDynamicSize(bool d = true); 194 | void setLeaveAfterClick(bool l = true); 195 | void setDoubleClicked(bool e = true); 196 | void setAutoTextColor(bool a = true); 197 | void setPretendFocus(bool f = true); 198 | void setBlockHover(bool b = true); 199 | 200 | void setShowAni(bool enable = true); 201 | void showForeground(); 202 | void showForeground2(QPoint point = QPoint(0, 0)); 203 | void hideForeground(); 204 | void delayShowed(int time, QPoint point = QPoint(0, 0)); 205 | 206 | QString getText(); 207 | void setMenu(QMenu *menu); 208 | void adjustMinimumSize(); 209 | void setState(bool s = true); 210 | bool getState(); 211 | virtual void simulateStatePress(bool s = true, bool a = false); 212 | bool isHovering() { return hovering; } 213 | bool isPressing() { return pressing; } 214 | void simulateHover(); 215 | void discardHoverPress(bool force = false); 216 | 217 | bool getSelfEnabled() { return self_enabled; } 218 | bool getParentEnabled() { return parent_enabled; } 219 | bool getForeEnabled() { return fore_enabled; } 220 | QColor getIconColor() { return icon_color; } 221 | QColor getTextColor() { return text_color; } 222 | QColor getNormalColor() { return normal_bg; } 223 | QColor getBorderColor() { return border_bg; } 224 | QColor getHoverColor() { return hover_bg; } 225 | QColor getPressColor() { return press_bg; } 226 | QString getIconPath() { return ""; } 227 | QString getPixmapPath() { return ""; } 228 | int getHoverAniDuration() { return hover_bg_duration; } 229 | int getPressAniDuration() { return press_bg_duration; } 230 | int getClickAniDuration() { return click_ani_duration; } 231 | double getIconPaddingProper() { return icon_padding_proper; } 232 | int getRadius() { return qMax(radius_x, radius_y); } 233 | int getBorderWidth() { return border_width; } 234 | bool getFixedTextPos() { return fixed_fore_pos; } 235 | bool getTextDynamicSize() { return text_dynamic_size; } 236 | bool getLeaveAfterClick() { return leave_after_clicked; } 237 | bool getShowAni() { return show_animation; } 238 | bool getWaterRipple() { return water_animation; } 239 | 240 | #if QT_DEPRECATED_SINCE(5, 11) 241 | QT_DEPRECATED_X("Use InteractiveButtonBase::setFixedForePos(bool fixed = true)") 242 | void setFixedTextPos(bool f = true); 243 | #endif 244 | 245 | protected: 246 | void enterEvent(QEvent *event) override; 247 | void leaveEvent(QEvent *event) override; 248 | void mousePressEvent(QMouseEvent *event) override; 249 | void mouseReleaseEvent(QMouseEvent *event) override; 250 | void mouseMoveEvent(QMouseEvent *event) override; 251 | void resizeEvent(QResizeEvent *event) override; 252 | void focusInEvent(QFocusEvent *event) override; 253 | void focusOutEvent(QFocusEvent *event) override; 254 | void changeEvent(QEvent *event) override; 255 | void paintEvent(QPaintEvent *event) override; 256 | 257 | virtual bool inArea(QPoint point); 258 | virtual bool inArea(QPointF point); 259 | virtual QPainterPath getBgPainterPath(); 260 | virtual QPainterPath getWaterPainterPath(Water water); 261 | virtual void drawIconBeforeText(QPainter &painter, QRect icon_rect); 262 | 263 | QRectF getUnifiedGeometry(); 264 | void updateUnifiedGeometry(); 265 | void paintWaterRipple(QPainter &painter); 266 | void setJitter(); 267 | 268 | int getFontSizeT(); 269 | void setFontSizeT(int f); 270 | 271 | int max(int a, int b) const; 272 | int min(int a, int b) const; 273 | int quick_sqrt(long X) const; 274 | qint64 getTimestamp() const; 275 | bool isLightColor(QColor color); 276 | int getSpringBackProgress(int x, int max); 277 | QColor getOpacityColor(QColor color, double level = 0.5); 278 | QPixmap getMaskPixmap(QPixmap p, QColor c); 279 | 280 | double getNolinearProg(int p, NolinearType type); 281 | QIcon::Mode getIconMode(); 282 | 283 | signals: 284 | void showAniFinished(); 285 | void hideAniFinished(); 286 | void pressAppearAniFinished(); 287 | void pressDisappearAniFinished(); 288 | void jitterAniFinished(); 289 | void doubleClicked(); 290 | void rightClicked(); 291 | void signalFocusIn(); 292 | void signalFocusOut(); 293 | 294 | void signalMouseEnter(); 295 | void signalMouseEnterLater(); // 进入后延迟信号(以渐变动画完成为准,相当于可手动设置) 296 | void signalMouseLeave(); 297 | void signalMouseLeaveLater(); // 离开后延迟的信号(直至渐变动画完成(要是划过一下子离开,这个也会变快)) 298 | void signalMousePress(QMouseEvent *event); 299 | void signalMousePressLater(QMouseEvent *event); 300 | void signalMouseRelease(QMouseEvent *event); 301 | void signalMouseReleaseLater(QMouseEvent *event); 302 | 303 | public slots: 304 | virtual void anchorTimeOut(); 305 | virtual void slotClicked(); 306 | void slotCloseState(); 307 | 308 | protected: 309 | PaintModel model; 310 | QIcon icon; 311 | QString text; 312 | QPixmap pixmap; 313 | PaintAddin paint_addin; 314 | EdgeVal fore_paddings; 315 | 316 | protected: 317 | // 总体开关 318 | bool self_enabled, parent_enabled, fore_enabled; // 是否启用子类、启动父类、绘制子类前景 319 | 320 | // 出现前景的动画 321 | bool show_animation, show_foreground; 322 | bool show_ani_appearing, show_ani_disappearing; 323 | int show_duration; 324 | qint64 show_timestamp, hide_timestamp; 325 | int show_ani_progress; 326 | QPointF show_ani_point; 327 | QRectF paint_rect; 328 | 329 | // 鼠标开始悬浮、按下、松开、离开的坐标和时间戳 330 | // 鼠标锚点、目标锚点、当前锚点的坐标;当前XY的偏移量 331 | QPointF enter_pos, press_pos, release_pos, mouse_pos, anchor_pos /*目标锚点渐渐靠近鼠标*/; 332 | QPointF offset_pos /*当前偏移量*/, effect_pos, release_offset; // 相对中心、相对左上角、弹起时的平方根偏移 333 | bool hovering, pressing; // 是否悬浮和按下的状态机 334 | qint64 hover_timestamp, leave_timestamp, press_timestamp, release_timestamp; // 各种事件的时间戳 335 | int hover_bg_duration, press_bg_duration, click_ani_duration; // 各种动画时长 336 | 337 | // 定时刷新界面(保证动画持续) 338 | QTimer *anchor_timer; 339 | int move_speed; 340 | 341 | // 背景与前景 342 | QColor icon_color, text_color; // 前景颜色 343 | QColor normal_bg, hover_bg, press_bg, border_bg; // 各种背景颜色 344 | QColor focus_bg, focus_border; // 有焦点的颜色 345 | int hover_speed, press_start, press_speed; // 颜色渐变速度 346 | int hover_progress, press_progress; // 颜色渐变进度 347 | double icon_padding_proper; // 图标的大小比例 348 | int icon_text_padding, icon_text_size; // 图标+文字模式共存时,两者间隔、图标大小 349 | int border_width; 350 | int radius_x, radius_y; 351 | int font_size; 352 | bool fixed_fore_pos; // 鼠标进入时是否固定文字位置 353 | bool fixed_fore_size; // 鼠标进入/点击时是否固定前景大小 354 | bool text_dynamic_size; // 设置字体时自动调整最小宽高 355 | bool auto_text_color; // 动画时是否自动调整文字颜色 356 | bool focusing; // 是否获得了焦点 357 | 358 | // 鼠标单击动画 359 | bool click_ani_appearing, click_ani_disappearing; // 是否正在按下的动画效果中 360 | int click_ani_progress; // 按下的进度(使用时间差计算) 361 | QMouseEvent *mouse_press_event, *mouse_release_event; 362 | 363 | // 统一绘制图标的区域(从整个按钮变为中心三分之二,并且根据偏移计算) 364 | bool unified_geometry; // 上面用不到的话,这个也用不到…… 365 | double _l, _t, _w, _h; 366 | 367 | // 鼠标拖拽弹起来回抖动效果 368 | bool jitter_animation; // 是否开启鼠标松开时的抖动效果 369 | double elastic_coefficient; // 弹性系数 370 | QList jitters; 371 | int jitter_duration; // 抖动一次,多次效果叠加 372 | 373 | // 鼠标按下水波纹动画效果 374 | bool water_animation; // 是否开启水波纹动画 375 | QList waters; 376 | int water_press_duration, water_release_duration, water_finish_duration; 377 | int water_radius; 378 | 379 | // 其他效果 380 | Qt::Alignment align; // 文字/图标对其方向 381 | bool _state; // 一个记录状态的变量,比如是否持续 382 | bool leave_after_clicked; // 鼠标单击松开后取消悬浮效果(针对菜单、弹窗),按钮必定失去焦点 383 | bool _block_hover; // 如果有出现动画,临时屏蔽hovering效果 384 | 385 | // 双击 386 | bool double_clicked; // 开启双击 387 | QTimer *double_timer; // 双击时钟 388 | bool double_prevent; // 双击阻止单击release的flag 389 | }; 390 | 391 | #endif // INTERACTIVEBUTTONBASE_H 392 | -------------------------------------------------------------------------------- /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 "interactivebuttonbase.h" 5 | 6 | class WaterCircleButton : public InteractiveButtonBase 7 | { 8 | public: 9 | WaterCircleButton(QWidget* parent = nullptr); 10 | WaterCircleButton(QIcon icon, QWidget* parent = nullptr); 11 | WaterCircleButton(QPixmap pixmap, QWidget* parent = nullptr); 12 | 13 | protected: 14 | void enterEvent(QEvent* event) override; 15 | void leaveEvent(QEvent* event) override; 16 | void mousePressEvent(QMouseEvent* event) override; 17 | void mouseReleaseEvent(QMouseEvent* event) override; 18 | void mouseMoveEvent(QMouseEvent* event) override; 19 | void resizeEvent(QResizeEvent* event) override; 20 | 21 | QPainterPath getBgPainterPath() override; 22 | QPainterPath getWaterPainterPath(Water water) override; 23 | 24 | void simulateStatePress(bool s = true); 25 | bool inArea(QPoint point) override; 26 | 27 | protected: 28 | QPoint center_pos; 29 | bool in_circle; 30 | int radius; 31 | }; 32 | 33 | #endif // WATERCIRCLEBUTTON_H 34 | -------------------------------------------------------------------------------- /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 | FacileMenu* fileMenu = new FacileMenu(this); 12 | fileMenu->addAction("新建"); 13 | fileMenu->addAction("打开"); 14 | fileMenu->addAction("保存"); 15 | fileMenu->addAction("另存为"); 16 | fileMenu->split()->addAction("退出"); 17 | 18 | FacileMenu* editMenu = new FacileMenu(this); 19 | editMenu->addAction("撤销"); 20 | editMenu->addAction("重做"); 21 | editMenu->split()->addAction("查找"); 22 | editMenu->addAction("替换"); 23 | FacileMenu* m = editMenu->split()->addMenu("编码"); 24 | m->addAction("ANSI"); 25 | m->addAction("UTF-8"); 26 | m->addAction("GBK"); 27 | 28 | FacileMenu* formatMenu = new FacileMenu(this); 29 | formatMenu->addAction("自动换行"); 30 | 31 | FacileMenu* viewMenu = new FacileMenu(this); 32 | m = viewMenu->addMenu("缩放"); 33 | m->addAction("放大"); 34 | m->addAction("缩小"); 35 | m->addAction("回复默认"); 36 | viewMenu->addAction("全屏"); 37 | 38 | FacileMenu* helpMenu = new FacileMenu(this); 39 | helpMenu->addAction("帮助"); 40 | helpMenu->addAction("反馈"); 41 | helpMenu->split()->addAction("关于"); 42 | 43 | ui->menuBar->setAnimationEnabled(false); // 开启动画。有闪烁的问题,受得了就能开 44 | ui->menuBar->addMenu("文件", fileMenu); 45 | ui->menuBar->addMenu("编辑", editMenu); 46 | ui->menuBar->addMenu("查看", viewMenu); 47 | ui->menuBar->addMenu("帮助", helpMenu); 48 | ui->menuBar->insertMenu(2, "格式", formatMenu); 49 | } 50 | 51 | MainWindow::~MainWindow() 52 | { 53 | delete ui; 54 | } 55 | 56 | void MainWindow::on_pushButton_clicked() 57 | { 58 | static bool faster_checked = true; 59 | static bool slower_checked = false; 60 | 61 | FacileMenu* menu = (new FacileMenu(this)) 62 | ->setTipArea("Ctrl+P") 63 | //->setSubMenuShowOnCursor(false) 64 | ->setSplitInRow(true); 65 | 66 | menu->addAction(QIcon(":/icons/run"), "开始播放 (&S)", [=]{ 67 | qDebug() << "=>开始播放"; 68 | })->tip("Ctrl+S"); 69 | 70 | menu->addAction(QIcon(":/icons/pause"), "暂停 (&P)", [=]{ 71 | qDebug() << "=>暂停"; 72 | })->tip("Ctrl+P")->suffix(true, "☆"); 73 | 74 | menu->addAction(QIcon(":/icons/resume"), "继续 (&R)", [=]{ 75 | qDebug() << "=>继续"; 76 | })->disable()->tip("Ctrl+R"); 77 | 78 | menu->addAction(QIcon(":/icons/stop"), "停止 (&T)", [=]{ 79 | qDebug() << "=>停止"; 80 | })->tip("Ctrl+T"); 81 | menu->addSeparator(); 82 | 83 | menu->addAction(QIcon(":/icons/faster"), "加速", [=]{ 84 | qDebug() << "=>加速"; 85 | faster_checked = !faster_checked; 86 | })->check(faster_checked)->linger() 87 | ->ifer(false) 88 | ->prefix("1") 89 | ->elser() 90 | ->prefix("2"); 91 | 92 | int number = 3; 93 | menu->addAction(QIcon(":/icons/slower"), "减速", [=]{ 94 | qDebug() << "=>减速"; 95 | slower_checked = !slower_checked; 96 | })->check(slower_checked)->linger() 97 | ->switcher(number) 98 | ->caser(1) 99 | ->suffix("1") 100 | ->breaker() 101 | ->caser(2) 102 | ->suffix("2") 103 | // ->breaker() 104 | ->caser(3) 105 | ->suffix(" caser") 106 | ->breaker() 107 | ->caser(4) 108 | ->suffix("4") 109 | ->breaker() 110 | ->caser(6, [=]{ qDebug() << "(^_−)☆"; }) 111 | ->defaulter() 112 | ->suffix("5") 113 | ->breaker(); 114 | 115 | menu->addRow([=]{ 116 | menu->addAction("按钮1"); 117 | menu->addAction("按钮2")->borderR(); 118 | menu->addAction("按钮3"); 119 | }); 120 | 121 | auto subMenu = menu->addMenu("子菜单0"); 122 | { 123 | subMenu->addAction(QIcon(":/icons/run"), "开始播放", [=]{ 124 | qDebug() << "=>开始播放"; 125 | }); 126 | 127 | subMenu->addAction(QIcon(":/icons/pause"), "暂停", [=]{ 128 | qDebug() << "=>暂停"; 129 | }); 130 | 131 | subMenu->addAction(QIcon(":/icons/resume"), "继续", [=]{ 132 | qDebug() << "=>继续"; 133 | })->disable(); 134 | 135 | subMenu->addAction(QIcon(":/icons/stop"), "停止", [=]{ 136 | qDebug() << "=>停止"; 137 | }); 138 | 139 | subMenu->addAction(QIcon(":/icons/faster"), "加速", [=]{ 140 | qDebug() << "=>加速"; 141 | faster_checked = !faster_checked; 142 | })->setChecked(faster_checked); 143 | 144 | auto subMenu2 = subMenu->addMenu("子菜单"); 145 | { 146 | subMenu2->addAction(QIcon(":/icons/run"), "开始播放", [=]{ 147 | qDebug() << "=>开始播放"; 148 | }); 149 | 150 | subMenu2->addAction(QIcon(":/icons/pause"), "暂停", [=]{ 151 | qDebug() << "=>暂停"; 152 | }); 153 | 154 | subMenu2->addAction(QIcon(":/icons/resume"), "继续", [=]{ 155 | qDebug() << "=>继续"; 156 | })->disable(); 157 | 158 | subMenu2->addAction(QIcon(":/icons/stop"), "停止", [=]{ 159 | qDebug() << "=>停止"; 160 | }); 161 | 162 | subMenu2->addAction(QIcon(":/icons/faster"), "加速", [=]{ 163 | qDebug() << "=>加速"; 164 | faster_checked = !faster_checked; 165 | })->setChecked(faster_checked); 166 | 167 | auto subMenu3 = subMenu2->addMenu("子菜单"); 168 | subMenu3->addAction("没有子菜单了"); 169 | } 170 | } 171 | 172 | auto subMenu2 = menu->addMenu("单选菜单"); 173 | { 174 | auto ac1 = subMenu2->addAction(QIcon(":/icons/run"), "带图标")->check()->linger(); 175 | auto ac2 = subMenu2->addAction("无图标")->uncheck()->linger(); 176 | auto ac3 = subMenu2->split()->addAction("全不选")->uncheck()->linger(); 177 | 178 | // 连接点击事件 179 | ac1->triggered([=]{ 180 | subMenu2->singleCheck(ac1); // 用于单选,表示只选中ac1 181 | // 这里可以用于处理其他操作 182 | }); 183 | ac2->triggered([=]{ 184 | subMenu2->singleCheck(ac2); 185 | }); 186 | ac3->triggered([=]{ 187 | subMenu2->uncheckAll(); // 全不选 188 | }); 189 | } 190 | 191 | auto subMenu4 = menu->addMenu("快速批量单选项"); 192 | { 193 | QStringList texts; 194 | QList values; 195 | for (int i = 0; i < 10; i++) 196 | { 197 | int val = qrand() % 1000; 198 | texts << "项目"+QString::number(val); 199 | values << val; 200 | } 201 | static int selected = 2; 202 | 203 | subMenu4->addOptions(texts, selected, [=](int index){ 204 | qDebug() << "选中了:" << (selected = index) << ",值:" << values.at(index); 205 | }); 206 | // 这里不建议(也没有必要)修改checked状态,因为点了就隐藏掉了 207 | } 208 | 209 | auto subMenu5 = menu->addMenu("多选菜单"); 210 | { 211 | // 假装是某一个需要多选的属性 212 | QList* list = new QList(); 213 | 214 | for (int i = 0; i < 10; i++) 215 | { 216 | int val = qrand() % 1000; // 放入菜单项的自定义数据 217 | auto action = subMenu5->addAction("选项"+QString::number(val))->setData(val)->uncheck()->autoToggle()->linger(); 218 | action->triggered([=]{ 219 | // 自己的处理流程,例如调用某个外部的方法 220 | if (action->isChecked()) 221 | list->append(action->getData().toInt()); 222 | else 223 | list->removeOne(action->getData().toInt()); 224 | qDebug() << "当前选中的有:" << *list; 225 | }); 226 | } 227 | 228 | subMenu5->finished([=]{ 229 | if (list->size()) 230 | qDebug() << "多选 最终选中:" << *list; 231 | delete list; 232 | }); 233 | } 234 | 235 | auto subMenu6 = menu->addMenu("快速批量多选项"); 236 | { 237 | // 假装是某一个需要多选的属性 238 | QList* list = new QList(); 239 | subMenu6->addNumberedActions("选项%1", 0, 10) 240 | ->setMultiCheck([=](int index, bool checked){ 241 | if (checked) 242 | list->append(index); 243 | else 244 | list->removeOne(index); 245 | qDebug() << "当前选中的有:" << *list; 246 | }); 247 | } 248 | 249 | auto subMenu7 = menu->addMenu("极简批量多选项"); 250 | { 251 | QList* list = new QList(); 252 | subMenu7->addNumberedActions("选项%1", 0, 15)->setMultiCheck() 253 | ->finished([=]{ 254 | *list = subMenu7->checkedItemTexts(); 255 | if (list->size()) 256 | qDebug() << "极简多选项 最终选中:" << *list; 257 | }); 258 | } 259 | 260 | auto subMenu8 = menu->addMenu("批量数字项"); 261 | { 262 | subMenu8->addNumberedActions("选项%1", 3, 13, [&](FacileMenuItem* item, int i){ 263 | item->setChecked(i%5==0)->linger()->autoToggle(); 264 | })->finished([=]{ 265 | auto list = subMenu8->checkedItemTexts(); 266 | if (list.size() != 2) 267 | qDebug() << "批量数字项 最终选中:" << list; 268 | }); 269 | } 270 | 271 | auto subMenu3 = menu->addMenu("判断语句"); 272 | { 273 | static bool b = false; 274 | subMenu3->addAction("if else 判断") 275 | ->tooltip("试一下 ifer elser") 276 | ->text(b, "当前:if", "当前:else") 277 | ->triggered([=]{b = !b;}) 278 | ->ifer(b) 279 | ->check() 280 | ->elser() 281 | ->uncheck(); 282 | } 283 | 284 | // 导入QMenu和QAction 285 | { 286 | QMenu* m = new QMenu("QMenu菜单", this); 287 | QAction* action1 = new QAction("子action1", this); 288 | QAction* action2 = new QAction("子action2", this); 289 | QAction* action3 = new QAction("子action3", this); 290 | m->addAction(action1); 291 | m->addAction(action2); 292 | m->addAction(action3); 293 | QMenu* m2 = new QMenu("QMenu菜单2", this); 294 | QAction* action21 = new QAction("子action21", this); 295 | QAction* action22 = new QAction("子action22", this); 296 | QAction* action23 = new QAction("子action23", this); 297 | m2->addAction(action21); 298 | m2->addAction(action22); 299 | m2->addAction(action23); 300 | action2->setMenu(m2); 301 | connect(action1, &QAction::triggered, this, [=]{ qDebug() << "action1.triggered"; }); 302 | connect(action21, &QAction::triggered, this, [=]{ qDebug() << "action21.triggered"; }); 303 | 304 | menu->addMenu(m); 305 | } 306 | 307 | menu->exec(QCursor::pos()); 308 | } 309 | 310 | void MainWindow::on_pushButton_2_clicked() 311 | { 312 | static bool faster_checked = true; 313 | static bool slower_checked = false; 314 | 315 | FacileMenu* menu = new FacileMenu(this); 316 | menu->setTipArea("Ctrl+P"); 317 | 318 | menu->addRow([=]{ 319 | menu->addAction("按钮1")->disable(); 320 | menu->addAction("按钮2")->disable(); 321 | menu->addAction("按钮3")->text(true, "按钮3 (&K)"); 322 | }); 323 | 324 | menu->addAction(QIcon(":/icons/run"), "开始播放 (&S)", [=]{ 325 | qDebug() << "=>开始播放"; 326 | })->tip("Ctrl+S")->fgColor(Qt::blue); 327 | 328 | menu->addAction(QIcon(":/icons/pause"), "暂停 (&P)", [=]{ 329 | qDebug() << "=>暂停"; 330 | })->tip("Ctrl+P")->bgColor(true, Qt::gray); 331 | 332 | QPushButton* button = new QPushButton("外部添加的按钮", this); 333 | menu->addWidget(button); 334 | 335 | QHBoxLayout* btn_hlayout = new QHBoxLayout; 336 | QPushButton* btn1= new QPushButton("按钮1", this); 337 | QPushButton* btn2 = new QPushButton("按钮2", this); 338 | btn_hlayout->addSpacing(40); 339 | btn_hlayout->addWidget(btn1); 340 | btn_hlayout->addItem(new QSpacerItem(4, 0)); 341 | btn_hlayout->addWidget(btn2); 342 | menu->addLayout(btn_hlayout); 343 | 344 | menu->addAction(QIcon(":/icons/resume"), "继续 (&R)", [=]{ 345 | qDebug() << "=>继续"; 346 | })->disable()->tip("Ctrl+R"); 347 | 348 | menu->addAction(QIcon(":/icons/stop"), "停止 (&T)", [=]{ 349 | qDebug() << "=>停止"; 350 | })->tip("Ctrl+T"); 351 | menu->addSeparator(); 352 | 353 | menu->addAction(QIcon(), "静态回调方法", staticFunction); 354 | // menu->fun2(1, "类内回调方法", this, &MainWindow::classFunction); 355 | 356 | menu->addAction(QIcon(":/icons/faster"), "加速", [=]{ 357 | qDebug() << "=>加速"; 358 | faster_checked = !faster_checked; 359 | })->setChecked(faster_checked); 360 | 361 | menu->addAction(QIcon(":/icons/slower"), "减速", [=]{ 362 | qDebug() << "=>减速"; 363 | slower_checked = !slower_checked; 364 | })->setChecked(slower_checked); 365 | 366 | menu->beginRow(); 367 | menu->addAction(QIcon(":/icons/run")); 368 | menu->addAction(QIcon(":/icons/pause")); 369 | menu->split()->addAction(QIcon(":/icons/resume")); 370 | menu->addAction(QIcon(":/icons/stop"))->disable(); 371 | menu->endRow(); 372 | 373 | menu->addAction(QIcon(":/icons/faster"), "加速", [=]{ 374 | qDebug() << "=>加速"; 375 | faster_checked = !faster_checked; 376 | })->setChecked(faster_checked)->disable(); 377 | 378 | menu->addAction(QIcon(":/icons/slower"), "减速", [=]{ 379 | qDebug() << "=>减速"; 380 | slower_checked = !slower_checked; 381 | })->setChecked(slower_checked)->disable(); 382 | 383 | menu->beginRow(); 384 | menu->addAction("按钮1"); 385 | menu->addAction("按钮2")->disable(); 386 | menu->addAction("按钮3")->disable(); 387 | menu->endRow(); 388 | 389 | menu->exec(QRect(mapToGlobal(ui->pushButton_2->pos()), ui->pushButton_2->size()), true, QCursor::pos()); 390 | } 391 | 392 | void MainWindow::staticFunction() 393 | { 394 | qDebug() << "静态方法回调"; 395 | } 396 | 397 | void MainWindow::classFunction() 398 | { 399 | qDebug() << "类内方法回调"; 400 | } 401 | 402 | void MainWindow::on_pushButton_3_clicked() 403 | { 404 | 405 | FacileMenu* menu = (new FacileMenu(this)); 406 | 407 | for (int i = 0; i < 80; i++) 408 | { 409 | menu->addAction("这是一个菜单" + QString::number(i)); 410 | } 411 | 412 | menu->exec(QCursor::pos()); 413 | } 414 | -------------------------------------------------------------------------------- /mainwindow.h: -------------------------------------------------------------------------------- 1 | #ifndef MAINWINDOW_H 2 | #define MAINWINDOW_H 3 | 4 | #include 5 | #include 6 | #include "facilemenu.h" 7 | 8 | QT_BEGIN_NAMESPACE 9 | namespace Ui { class MainWindow; } 10 | QT_END_NAMESPACE 11 | 12 | class MainWindow : public QMainWindow 13 | { 14 | Q_OBJECT 15 | public: 16 | MainWindow(QWidget *parent = nullptr); 17 | ~MainWindow(); 18 | 19 | public slots: 20 | void on_pushButton_clicked(); 21 | 22 | void on_pushButton_2_clicked(); 23 | 24 | static void staticFunction(); 25 | void classFunction(); 26 | 27 | private slots: 28 | void on_pushButton_3_clicked(); 29 | 30 | private: 31 | Ui::MainWindow *ui; 32 | }; 33 | #endif // MAINWINDOW_H 34 | -------------------------------------------------------------------------------- /mainwindow.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | MainWindow 4 | 5 | 6 | 7 | 0 8 | 0 9 | 381 10 | 179 11 | 12 | 13 | 14 | MainWindow 15 | 16 | 17 | 18 | 19 | 20 | 30 21 | 80 22 | 75 23 | 23 24 | 25 | 26 | 27 | 菜单 28 | 29 | 30 | 31 | 32 | 33 | 120 34 | 80 35 | 75 36 | 23 37 | 38 | 39 | 40 | 菜单2 41 | 42 | 43 | 44 | 45 | 46 | 210 47 | 80 48 | 75 49 | 23 50 | 51 | 52 | 53 | 菜单3 54 | 55 | 56 | 57 | 58 | 59 | 0 60 | 0 61 | 341 62 | 31 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 0 71 | 0 72 | 381 73 | 21 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | InteractiveButtonBase 82 | QPushButton 83 |
interactivebuttonbase.h
84 |
85 | 86 | FacileMenuBar 87 | QWidget 88 |
facilemenubar.h
89 | 1 90 |
91 |
92 | 93 | 94 |
95 | -------------------------------------------------------------------------------- /resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | resources/icons/faster.png 4 | resources/icons/pause.png 5 | resources/icons/resume.png 6 | resources/icons/run.png 7 | resources/icons/slower.png 8 | resources/icons/stop.png 9 | resources/icons/sub_menu_arrow.png 10 | 11 | 12 | -------------------------------------------------------------------------------- /resources/icons/faster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Qt-Widgets/Qt-FacileMenu-Custom-Animated-Context-Menu-Animation/a62dec55c13a40834f13245fece93f1be240e6f5/resources/icons/faster.png -------------------------------------------------------------------------------- /resources/icons/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Qt-Widgets/Qt-FacileMenu-Custom-Animated-Context-Menu-Animation/a62dec55c13a40834f13245fece93f1be240e6f5/resources/icons/pause.png -------------------------------------------------------------------------------- /resources/icons/resume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Qt-Widgets/Qt-FacileMenu-Custom-Animated-Context-Menu-Animation/a62dec55c13a40834f13245fece93f1be240e6f5/resources/icons/resume.png -------------------------------------------------------------------------------- /resources/icons/run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Qt-Widgets/Qt-FacileMenu-Custom-Animated-Context-Menu-Animation/a62dec55c13a40834f13245fece93f1be240e6f5/resources/icons/run.png -------------------------------------------------------------------------------- /resources/icons/slower.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Qt-Widgets/Qt-FacileMenu-Custom-Animated-Context-Menu-Animation/a62dec55c13a40834f13245fece93f1be240e6f5/resources/icons/slower.png -------------------------------------------------------------------------------- /resources/icons/stop.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Qt-Widgets/Qt-FacileMenu-Custom-Animated-Context-Menu-Animation/a62dec55c13a40834f13245fece93f1be240e6f5/resources/icons/stop.png -------------------------------------------------------------------------------- /resources/icons/sub_menu_arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Qt-Widgets/Qt-FacileMenu-Custom-Animated-Context-Menu-Animation/a62dec55c13a40834f13245fece93f1be240e6f5/resources/icons/sub_menu_arrow.png -------------------------------------------------------------------------------- /screenshots/menubar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Qt-Widgets/Qt-FacileMenu-Custom-Animated-Context-Menu-Animation/a62dec55c13a40834f13245fece93f1be240e6f5/screenshots/menubar.gif -------------------------------------------------------------------------------- /screenshots/picture.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Qt-Widgets/Qt-FacileMenu-Custom-Animated-Context-Menu-Animation/a62dec55c13a40834f13245fece93f1be240e6f5/screenshots/picture.gif --------------------------------------------------------------------------------