Windows+Qt 简单表达式计算器实现
学校数据结构课程刚学完了栈,要求做一个简单的表达式计算器,要求能够有一定的错误识别能力、能够实时显示栈中的元素以及最好有UI界面。
由于最近正好在学习Qt的知识,很多功能都不是特别熟悉,决定正好学习一下Qt中的无边框窗口、背景模糊以及动画效果。整个工程完成时间大概是6天,写此文档做一些记录和分享。
实现思路
由于上一次Project实现中偷懒把一堆函数都混在了窗口类中,导致很多函数的调用都不是很方便,甚至改起来让人崩溃。所以这一次尝试了一些新的思路:
- 把需要实行特定功能的控件独立出来,单独写它们的.h和.cpp文件,并且提供功能接口给父控件
- 核心功能的函数也同样独立出来,将表达式作为一个类来处理, 求值、退格、输入、清零都是该类中的方法,并提供了接口供UI界面调用
- 将各个功能的控件在其他工程中单独测试, 确认没有Bug再移植进已有的界面
不得不说,这三点确实极大地简化了我的维护成本,后来碰到Bug基本一两分钟就能定位到出错代码位置,极大地提高了程序可读性和写程序的效率。
回到程序本身,这次Project决定直接对Windows系统计算器进行复刻,简单分析后将任务拆分为如下部分:
- 核心功能:表达式的创建、输入、计算, 操作数(符)栈的维护、更新,结果和输入的QString格式输出函数等。
- 无窗口对话框:实现背景模糊、无边框的对话框,用来做程序窗口主体
- 透明按钮类:实现输入区域需要用到的透明按钮,实现
enterEvent()
和leaveEvent()
以及mousePressEvent()
触发下的变色效果 - 标签页切换类:实现一个管理页面控件的Widget来支持多页面切换动画
- 滚动列表类:实现一个具有松手滑动、边界回弹、具有漂亮的半透明滚动指示条(Qt原生的太大了)、支持
AddWidget()
、RemoveWidget()
接口以及添加、删除控件的动画效果的列表类。
实现原理
接下来依次简单记录一下这些部分的完成原理
核心功能
表达式运算
- 分别建立操作数栈和操作符栈
- 预先将运算符ANCII值作为key,优先级作为value存在哈希表中
- 比较输入操作符和栈顶操作符优先级,不断对栈进行弹出并运算操作直到栈顶操作符优先级小于输入操作符
- 压入输入操作符
部分核心代码
void Expression::update(int index = -1){
//index is the priority of input operator
while(numStack.Size() > 1 && !opStack.IsEmpty() && opMap[opStack.TopElement()] >= index && opStack.TopElement() != '('){
double num2 = numStack.PopBack();
double num1 = numStack.PopBack();
char op = opStack.PopBack();
double res = calc(num1, num2, op);
numStack.PushBack(res);
}
}
double calc(const double& num1, const double& num2, const char& op){
double res;
switch(op){
case '+':
res = num1 + num2;
break;
case '-':
res = num1 - num2;
break;
case '*':
res = num1 * num2;
break;
case '/':
res = num1 / num2;
break;
case '%':
//may occur error answer!
if((int)num2 == 0)
res = std::nanf("1");
else
res = (int)num1 % (int)num2;
break;
case '^':
res = pow(num1, num2);
break;
case '.': {
QString integer(QString::asprintf("%d", (int)num1));
QString fracture(QString::asprintf("%d", (int)num2));
fracture.remove(0, 1); //remove the first 0
QString num;
num.append(integer);
num.append('.');
num.append(fracture);
res = num.toDouble();
break;
}
default:
res = 0;
break;
}
return res;
}
小数点的处理
在处理输入的时候,遇到了一个稍微麻烦一点的问题:小数点,处理逻辑如下
- 将小数点优先级设为最高
- 输入小数点后,立即向运算数栈中压入1
- 正常输入小数
- 输入任一操作符或更新栈操作都将进行小数运算
压入1的原因:
在处理一般整数输入的时候采用了如下逻辑:
如果当前输入为整数第一位
,则直接压入栈
如果当前输入非整数第一位
,则弹出栈顶数,运算栈顶数×10+当前数
,再压入栈顶
若不在小数点后压入1
对于X.0003
这样的输入0
会被忽略
读为错误的X.3
压入1后在运算时再去掉头上的1即可解决这个问题
实时判断算式合法性
思考了一下,发现判断的情况很多,突然想到位运算这一操作。于是有如下核心原理:
设置一个二进制flag
,每一位都表示对某一种输入是否accept
,在每一次输入后更新这个flag
000000
:不接受任何输入100000
:接受(
010000
:接受操作符001000
:接受数字000100
:接受.
000010
:接受小数输入000001
:接受-
flag
表如下:
核心代码如下:
void Expression::insert(char c){
//clear last step differences in stacks
opChange.clear();
numChange.clear();
if(c == '#'){
update();
return;
}
if(!errorInput.IsEmpty())
//Error stack is not empty, regard as error input.
errorInput.PushBack(c);
else if(c == '('){
if(inputCons & 0b100000){
opStack.PushBack(c);
opChange.append(QString::asprintf("i%c", c));
//allow minus numbers
inputCons |= 0b000001;
}
else
errorInput.PushBack(c);
}
else if(c == ')'){
if(inputCons & 0b010000){
update();
if(opStack.TopElement() == '('){
opStack.PopBack();
opChange.append('o');
//set constraints
inputCons &= 0b010110;
}
else
errorInput.PushBack(c);
}
else
errorInput.PushBack(c);
}
else if(c == '.'){
if((inputCons & 0b000110) == 0b000110){
//accept dot
opStack.PushBack(c);
numStack.PushBack(1);
opChange.append(QString::asprintf("i%c", c));
numChange.append("i1");
//stop accept dots and operators
inputCons &= 0b001010;
}
else
errorInput.PushBack(c);
}
else if(c >= '0' && c <= '9'){
if(inputCons & 0b001000){
if(inputCons & 0b010000 || opStack.TopElement() == '.'){
//not the first number
double i = numStack.PopBack();
i *= 10;
i += (c - '0');
numStack.PushBack(i);
numChange.append(QString::asprintf("oi%lf", i));
}
else{
double i = c - '0';
numStack.PushBack(i);
//qDebug() << c - '0' << "\n";
numChange.append(QString::asprintf("i%lf", i));
}
inputCons &= 0b011110;
inputCons |= 0b010000;
if(opStack.TopElement() != '.')
inputCons |= 0b000100;
}
else
errorInput.PushBack(c);
}
else if(opMap[c] >= 0){
if(inputCons & 0b010000){
update(opMap[c]);
if(c == '%' && numStack.TopElement() != (int)numStack.TopElement())
errorInput.PushBack(c);
else{
opStack.PushBack(c);
opChange.append(QString::asprintf("i%c", c));
//set constraints
inputCons &= 0b101010;
inputCons |= 0b101000;
if(c == '%')
inputCons &= 0b111101;
}
}
else{
if(c == '-' && inputCons & 0b000001){
//adjust exp string
if(opStack.TopElement() != '(')
expString.append('0');
//accept minus number
numStack.PushBack(0);
opStack.PushBack('-');
numChange.append("i0");
opChange.append("i-");
//stop accepting minus numbers and operators
inputCons &= 0b101110;
//allow ( and integers
inputCons |= 0b101000;
}
else
errorInput.PushBack(c);
}
}
expString.append(c);
}
至此,核心原理基本差不多了,下面简略说一下UI:
UI设计
由于时间原因,先贴一些关键代码,后面逐步完善讲解~
无边框窗口和Win10下的背景模糊
无边框使用的是Qt中setAttribute()
以及setWindowFlags()
函数
setWindowFlags(Qt::FramelessWindowHint)
:设置无边框
setAttribute(Qt::WA_TranslucentBackground)
:设置窗口背景透明
这里需要注意的是,设置了setAttribute(Qt::WA_TranslucentBackground)
后,整个窗口不仅是透明的,还完全不能点击,因此需要重新写窗口的paintEvent()
事件为窗口绘制一个底色,圆角窗口也同样是重写该函数实现
void Acrylic::paintEvent(QPaintEvent *event){
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing);
QPen pen;
pen.setColor(QColor(0,0,0,255));
pen.setStyle(Qt::SolidLine);
pen.setWidth(1);
painter.setPen(pen);
painter.setBrush(acryBackground);
painter.drawRect(this->rect());
ui->TitleBar->resize(this->rect().width() - 10, ui->TitleBar->height());
}
背景模糊并不是我自己写的,查阅了很多文档之后,发现大多都是使用了一个Windows隐藏的API来实现,大致实现方式如下:
在工程中添加WindowCompositionAttribute.h
文件
#pragma once
#include <Windows.h>
typedef enum _WINDOWCOMPOSITIONATTRIB
{
WCA_UNDEFINED = 0,
WCA_NCRENDERING_ENABLED = 1,
WCA_NCRENDERING_POLICY = 2,
WCA_TRANSITIONS_FORCEDISABLED = 3,
WCA_ALLOW_NCPAINT = 4,
WCA_CAPTION_BUTTON_BOUNDS = 5,
WCA_NONCLIENT_RTL_LAYOUT = 6,
WCA_FORCE_ICONIC_REPRESENTATION = 7,
WCA_EXTENDED_FRAME_BOUNDS = 8,
WCA_HAS_ICONIC_BITMAP = 9,
WCA_THEME_ATTRIBUTES = 10,
WCA_NCRENDERING_EXILED = 11,
WCA_NCADORNMENTINFO = 12,
WCA_EXCLUDED_FROM_LIVEPREVIEW = 13,
WCA_VIDEO_OVERLAY_ACTIVE = 14,
WCA_FORCE_ACTIVEWINDOW_APPEARANCE = 15,
WCA_DISALLOW_PEEK = 16,
WCA_CLOAK = 17,
WCA_CLOAKED = 18,
WCA_ACCENT_POLICY = 19,
WCA_FREEZE_REPRESENTATION = 20,
WCA_EVER_UNCLOAKED = 21,
WCA_VISUAL_OWNER = 22,
WCA_LAST = 23
} WINDOWCOMPOSITIONATTRIB;
typedef struct _WINDOWCOMPOSITIONATTRIBDATA
{
WINDOWCOMPOSITIONATTRIB Attrib;
PVOID pvData;
SIZE_T cbData;
} WINDOWCOMPOSITIONATTRIBDATA;
typedef enum _ACCENT_STATE
{
ACCENT_DISABLED = 0,
ACCENT_ENABLE_GRADIENT = 1,
ACCENT_ENABLE_TRANSPARENTGRADIENT = 2,
ACCENT_ENABLE_BLURBEHIND = 3,
ACCENT_ENABLE_ACRYLICBLURBEHIND = 4,
ACCENT_INVALID_STATE = 5
} ACCENT_STATE;
typedef struct _ACCENT_POLICY
{
ACCENT_STATE AccentState;
DWORD AccentFlags;
DWORD GradientColor;
DWORD AnimationId;
} ACCENT_POLICY;
WINUSERAPI
BOOL
WINAPI
GetWindowCompositionAttribute(
_In_ HWND hWnd,
_Inout_ WINDOWCOMPOSITIONATTRIBDATA* pAttrData);
typedef BOOL(WINAPI*pfnGetWindowCompositionAttribute)(HWND, WINDOWCOMPOSITIONATTRIBDATA*);
WINUSERAPI
BOOL
WINAPI
SetWindowCompositionAttribute(
_In_ HWND hWnd,
_Inout_ WINDOWCOMPOSITIONATTRIBDATA* pAttrData);
typedef BOOL(WINAPI*pfnSetWindowCompositionAttribute)(HWND, WINDOWCOMPOSITIONATTRIBDATA*);
在mainWindow.h(也就是你需要实现模糊的窗口的.h)
文件中添加如下代码块
#include "WindowCompositionAttribute.h"
/*
* Other codes
*/
//Inside class
private:
HWND hwnd;
HMODULE huser;
pfnSetWindowCompositionAttribute setWindowCompositionAttribute;
QColor acryBackground; //用来控制背景颜色
int acryOpacity; //用来控制透明度
在mainWindow.cpp(也就是你需要实现模糊的窗口的.cpp)
文件中添加如下代码块
p.s.亚克力和毛玻璃效果都是可以的,但是好像是因为这个API的原因,亚克力效果会非常卡,所以不建议使用
acryBackground = QColor(240, 240, 240, 150);
acryOpacity = 50;
hwnd = HWND(winId());
huser = GetModuleHandle(L"user32.dll");
//Areo Effect
if(huser){
setWindowCompositionAttribute = (pfnSetWindowCompositionAttribute)GetProcAddress(huser, "SetWindowCompositionAttribute");
if(setWindowCompositionAttribute){
//DWORD gradientColor = DWORD(0x50FFFFFF);
ACCENT_POLICY accent = { ACCENT_ENABLE_BLURBEHIND, 0, 0, 0 };
WINDOWCOMPOSITIONATTRIBDATA data;
data.Attrib = WCA_ACCENT_POLICY;
data.pvData = &accent;
data.cbData = sizeof(accent);
setWindowCompositionAttribute(hwnd, &data);
}
}
//Acrylic Effect
//Warning: Due to the API proplem, this effect is laggy when dragging | resizing
//if(huser){
// setWindowCompositionAttribute = (pfnSetWindowCompositionAttribute)GetProcAddress(huser, "SetWindowCompositionAttribute");
// if(setWindowCompositionAttribute){
// DWORD gradientColor = DWORD(0x50F5F5F5);
// ACCENT_POLICY accent = { ACCENT_ENABLE_ACRYLICBLURBEHIND, 0, gradientColor, 0 };
// WINDOWCOMPOSITIONATTRIBDATA data;
// data.Attrib = WCA_ACCENT_POLICY;
// data.pvData = &accent;
// data.cbData = sizeof(accent);
// setWindowCompositionAttribute(hwnd, &data);
// }
//}
透明按钮
同样是重写paintEvent()
事件为按钮绘制一个text
和矩形框
文字居中采用了QFontMetrics
的方法
查阅多个文档并试验后,
textPainter.fontMetrics().ascent() - textPainter.fontMetrics().descent() + textPainter.fontMetrics().leading()
这个表达式最贴近文字本身的高度
void QTransparentButton::paintEvent(QPaintEvent *event){
QPainter bgPainter(this);
bgPainter.setPen(Qt::NoPen);
bgPainter.setBrush(bgColor);
bgPainter.drawRect(this->rect());
buttonText = this->text();
//QPainter test(this);
//test.setPen(Qt::SolidLine);
//test.setBrush(Qt::NoBrush);
QPainter textPainter(this);
QFont textFont("FuturaNo2D", 14); //Set to "DengXian" on your computer
textPainter.setFont(textFont);
int widthOfText = textPainter.fontMetrics().size(Qt::TextSingleLine, buttonText).width();
int heightOfText = textPainter.fontMetrics().ascent() - textPainter.fontMetrics().descent() + textPainter.fontMetrics().leading();
textPainter.drawText(this->width() / 2 - widthOfText / 2, this->height() / 2 + heightOfText / 2, buttonText);
}
原创标签页切换组件
这个稍微有点复杂,首先先讲一个一开始困扰了我半天的问题:
如果想要对一个控件进行move()
或是设置动画等操作的话,一定不要把它放在布局里!
不然什么效果都没有!
==================
切换组件我命名为AdditionTabWidget
类(英文辣鸡不会取名了(捂脸
先看看基本布局:
-
主布局
iconContents
:用来存放页面的按钮(依旧词穷indicator
:用来指示当前页面(蓝蓝的横线pageContainer
:用来盛装各类页面,作为子页面的父组件,方便设置子页面坐标、动画等
QVBoxLayout* mainLayout
中的组件:
再看看主要变量和函数:
-
类
QVector<tabButtons>
:存放页面按钮组件指针的容器,方便 在按钮被点击时通过按钮的指针得到页面在容器中的位置(index
)QVector<pages>
:存放页面指针的容器,方便 直接通过index
值取到页面指针,方便添加动画、调整大小等操作currentPageIndex
:指示当前显示的页面的index
ShiftPage(TabIcons *)
:联动上面的容器,从按钮指针取得index
值,取出特定页面,添加动画,切换页面
AdditionTabWidget
中的主要变量和函数:
下面放代码:
additiontabwidget.h
头文件
根据代码中的注释提示添加需要起到转发作用的signal和slot函数
/* A simple tab widget written by Jonathan Zhang
* Github: haven't uploaded yet
*
* About this widget:
* A simple controller for different pages
* containing a tab bar to choose and a tiny indicator XD
* ALSO HAS ANIMATIONS! yes!
*/
#ifndef ADDITIONTABWIDGET_H
#define ADDITIONTABWIDGET_H
#include <QWidget>
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QGridLayout>
#include <QPainter>
#include "tabicons.h"
#include "tabindicator.h"
#include "tabpage.h"
#include <QVector>
#include <QPropertyAnimation>
#include <QPauseAnimation>
#include <QParallelAnimationGroup>
#include <QSequentialAnimationGroup>
#include <QGraphicsOpacityEffect>
class AdditionTabWidget : public QWidget
{
Q_OBJECT
private:
QVBoxLayout* widgetLayout;
QWidget* iconContents;
TabIndicator* indicator;
QWidget* pageContainer;
QVector<TabIcons*> tabButtons;
QVector<TabPage*> pages;
int currentPageIndex = -1;
void paintEvent(QPaintEvent* event);
public:
explicit AdditionTabWidget(QWidget *parent = nullptr);
signals:
/* Add Signals for pages here
* E.g.
* void YourSignal();
*/
private slots:
void ShiftPage(TabIcons* sel);
/* Add Slots for pages here
*
* These slots are used for receiving msg from outside this controller
* Remember to emit a signal from main dialog!
*
* REMEMBER TO CHECK THIS CONTROLLER'S PARENT IS SET TO YOUR SIGNAL WIDGET
*/
};
#endif // ADDITIONTABWIDGET_H
tabpage.h
头文件
由于原本以为会在Tabpage类中添加一些方法,后来发现好像啥也没添加,所以其实可以更改一下addtiontabwidget中的代码,让页面直接从QWidget类继承,从而不再需要添加tabpage文件,不过由于事件原因,我自己就先保留原本的样子不更改了。
#ifndef TABPAGE_H
#define TABPAGE_H
#include <QWidget>
class TabPage : public QWidget
{
Q_OBJECT
public:
explicit TabPage(QWidget *parent = nullptr);
signals:
};
#endif // TABPAGE_H
additiontabwidget.cpp
源文件
根据注释提示在适当的位置添加页面即可,仅仅只需4行代码!
#include "additiontabwidget.h"
AdditionTabWidget::AdditionTabWidget(QWidget *parent) : QWidget(parent)
{
this->setMouseTracking(true);
//this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
//initialize main layout
QVBoxLayout* mainLayout = new QVBoxLayout;
mainLayout->setContentsMargins(0, 0, 0, 0);
mainLayout->setSpacing(15);
this->setLayout(mainLayout);
//initialize tab bar
//tabBar = new QWidget(this);
//QVBoxLayout* tabBarVLayout = new QVBoxLayout;
//tabBarVLayout->setContentsMargins(5, 5, 5, 5);
//tabBar->setLayout(tabBarVLayout);
iconContents = new QWidget(this);
QHBoxLayout* iconHLayout = new QHBoxLayout;
iconHLayout->setContentsMargins(0, 0, 0, 0);
iconHLayout->setSpacing(0);
iconHLayout->setAlignment(Qt::AlignLeft);
iconContents->setLayout(iconHLayout);
iconContents->setMouseTracking(true);
//tabBarVLayout->addWidget(iconContents);
//initialize indicator
indicator = new TabIndicator(this);
indicator->resize(0, 3);
//indicator->move(-indicator->x(), iconContents->height() + 10);
//tabBarVLayout->addWidget(indicator);
//initialize page container
pageContainer = new QWidget(this);
//QGridLayout* pageLayout = new QGridLayout;
//pageLayout->setContentsMargins(0, 0, 0, 0);
//pageContainer->setLayout(pageLayout);
pageContainer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
pageContainer->setMouseTracking(true);
mainLayout->addWidget(iconContents);
mainLayout->addWidget(pageContainer);
/*********************************************************/
/* Add your pages here
* Remember to add icon in addition to pages
*
* Add a page like this:
* ------------------------------------------------------------------
* YourPage* newPage = new YourPage(pageContainer); //YourPage class should be inherited from TabPage class
* TabIcons* newPageLabel = new TabIcons("YourPageName", iconContents);
* pages.push_back(newPage);
* tabButtons.push_back(newPageLabel);
*
* QObject::connect() if you need
* ------------------------------------------------------------------
* And everything else just leave for the controller XD
*
*/
/*********************************************************/
//Initialize all pages
for(int i = 0; i < pages.size(); i++){
pages[i]->move(0, 0);
pages[i]->resize(pageContainer->size());
pages[i]->hide();
}
for(int i = 0; i < tabButtons.size(); i++){
iconHLayout->addWidget(tabButtons[i]);
QObject::connect(tabButtons[i], SIGNAL(SelectPage(TabIcons*)), this, SLOT(ShiftPage(TabIcons*)));
}
//Initialize default shown page and indicator
pages[0]->show();
tabButtons[0]->SetFocus(true);
indicator->resize(tabButtons[0]->size().width() - 30, 3);
indicator->move(15, iconContents->height() + 10);
indicator->raise();
currentPageIndex = 0;
}
void AdditionTabWidget::paintEvent(QPaintEvent *event){
if(currentPageIndex != -1){
pages[currentPageIndex]->resize(pageContainer->size());
//pages[currentPageIndex]->setMinimumWidth(pageContainer->width());
//pages[currentPageIndex]->setMaximumSize(pageContainer->size());
}
}
void AdditionTabWidget::ShiftPage(TabIcons* sel){
//get index
int index = tabButtons.indexOf(sel);
if(index == currentPageIndex)
return;
//choose new page and Add to page container
TabPage* newPage = pages[index];
TabPage* curPage = pages[currentPageIndex];
//prepare for animation
QGraphicsOpacityEffect* newPageOpac = new QGraphicsOpacityEffect;
QGraphicsOpacityEffect* curPageOpac = new QGraphicsOpacityEffect;
newPageOpac->setOpacity(0);
curPageOpac->setOpacity(1);
newPage->setGraphicsEffect(newPageOpac);
curPage->setGraphicsEffect(curPageOpac);
newPage->show();
newPage->raise();
indicator->raise();
QParallelAnimationGroup* switchPages = new QParallelAnimationGroup(pageContainer);
QSequentialAnimationGroup* FadeIn = new QSequentialAnimationGroup(pageContainer);
QPropertyAnimation* newPageFadeIn = new QPropertyAnimation(newPageOpac, "opacity", newPage);
QPropertyAnimation* newPageMove = new QPropertyAnimation(newPage, "pos");
QPropertyAnimation* curPageFadeOut = new QPropertyAnimation(curPageOpac, "opacity", curPage);
QPropertyAnimation* curPageMove = new QPropertyAnimation(curPage, "pos");
QPropertyAnimation* indicatorScale = new QPropertyAnimation(indicator, "geometry");
if(currentPageIndex < index){
//newPage->raise();
newPageFadeIn->setDuration(600);
newPageFadeIn->setStartValue(0);
newPageFadeIn->setEndValue(1);
newPageFadeIn->setEasingCurve(QEasingCurve::Linear);
FadeIn->addPause(300);
FadeIn->addAnimation(newPageFadeIn);
newPageMove->setDuration(1200);
newPageMove->setStartValue(QPoint(50, newPage->y()));
newPageMove->setEndValue(QPoint(0, newPage->y()));
newPageMove->setEasingCurve(QEasingCurve::InOutQuad);
curPageFadeOut->setDuration(500);
curPageFadeOut->setStartValue(1);
curPageFadeOut->setEndValue(0);
curPageFadeOut->setEasingCurve(QEasingCurve::Linear);
curPageMove->setDuration(800);
curPageMove->setStartValue(QPoint(0, curPage->y()));
curPageMove->setEndValue(QPoint(-30, curPage->y()));
curPageMove->setEasingCurve(QEasingCurve::InOutQuad);
indicatorScale->setDuration(800);
indicatorScale->setStartValue(QRect(indicator->x(), indicator->y(), indicator->width(), indicator->height()));
indicatorScale->setEndValue(QRect(tabButtons[index]->x() + 15, indicator->y(), tabButtons[index]->size().width() - 30, indicator->height()));
indicatorScale->setEasingCurve(QEasingCurve::InOutQuad);
}
else{
//curPage->raise();
newPageFadeIn->setDuration(600);
newPageFadeIn->setStartValue(0);
newPageFadeIn->setEndValue(1);
newPageFadeIn->setEasingCurve(QEasingCurve::Linear);
FadeIn->addPause(300);
FadeIn->addAnimation(newPageFadeIn);
newPageMove->setDuration(1200);
newPageMove->setStartValue(QPoint(-50, newPage->y()));
newPageMove->setEndValue(QPoint(0, newPage->y()));
newPageMove->setEasingCurve(QEasingCurve::InOutQuad);
curPageFadeOut->setDuration(500);
curPageFadeOut->setStartValue(1);
curPageFadeOut->setEndValue(0);
curPageFadeOut->setEasingCurve(QEasingCurve::Linear);
curPageMove->setDuration(800);
curPageMove->setStartValue(QPoint(0, curPage->y()));
curPageMove->setEndValue(QPoint(50, curPage->y()));
curPageMove->setEasingCurve(QEasingCurve::InOutQuad);
indicatorScale->setDuration(800);
indicatorScale->setStartValue(QRect(indicator->x(), indicator->y(), indicator->width(), indicator->height()));
indicatorScale->setEndValue(QRect(tabButtons[index]->x() + 15, indicator->y(), tabButtons[index]->size().width() - 30, indicator->height()));
indicatorScale->setEasingCurve(QEasingCurve::InOutQuad);
}
switchPages->addAnimation(FadeIn);
switchPages->addAnimation(newPageMove);
switchPages->addAnimation(curPageFadeOut);
switchPages->addAnimation(curPageMove);
switchPages->addAnimation(indicatorScale);
//start animation
switchPages->start();
connect(switchPages,&QPropertyAnimation::finished,[=](){newPageOpac->deleteLater();});
//indicator->resize(tabButtons[index]->size().width() - 30, indicator->height());
tabButtons[currentPageIndex]->SetFocus(false);
currentPageIndex = index;
tabButtons[index]->SetFocus(true);
update();
}
tabpage.cpp
源文件
#include "tabpage.h"
TabPage::TabPage(QWidget *parent) : QWidget(parent)
{
this->setMouseTracking(true);
}
使用方法:
- 把这些文件全部添加到工程
- 创建你的页面:从TabPage继承一个类作为你的页面,在该类中设计你的页面
- 通过根据
additiontabwidget.cpp
文件中注释的提示添加需要的四行代码来把你的页面添加到页面控制器里面去 - 动画效果都是自动生成的,直接用就好了
重写ScrollAreaWidget
这个堪称我最满意的作品
不过限于时间先不详细写了,需要的话可以直接文末链接跳转GitHub仓库查看源文件
-
实现的功能:
惯性滑动
:松开手以后还能滑动一段距离,逐渐减速到停止边界反弹
:滚动到边缘会继续滚动,以指数级减速至停止后线性反弹,实现一个反弹的特效,反弹距离会根据到达边界时候的速度变化边界阻尼
:超过边界继续滑动会有不断增加的阻尼,越来越难拖动鼠标滚轮滚动
:支持鼠标滚轮滚动重新绘制导航条
:重新绘制了导航条,可以在源代码中调整导航条与边界的距离、导航条默认宽度、鼠标放置在导航条上时的宽度、导航条颜色(默认、鼠标悬浮、鼠标点击)等,并且具有动画
实现效果与代码开源
实现效果截图
代码开源
这个项目的代码我已经公开在GitHub以及Gitee上,欢迎前往下载~
因为调用了Windows的API,所以工程应该是不能够在Mac OS或是Linux上面运行的,还请见谅
时间仓促,可能有很多隐藏的Bug没有测试出来,代码也有很多不规范的地方,还请各位大佬不吝赐教!
GitHub地址:GitHub: Acrylic_Expression_Calculator_in_Qt
Gitee地址:Gitee: Acrylic_Expression_Calculator_in_Qt