Home-made calculator from 0!

Keywords: C Qt calculator github git

Let's see the effect first:

Is it cool?

Do you want to?

Want it.

Of course the author knows you must.

Otherwise it won't come in at all, right.

Okay. Get to the point.

1. Overview

This is a simplified version made following the calculator that comes with win10. It is made with Qt and the whole expression is directly entered and the result is calculated.
It is mainly divided into three parts: interface part, event handling part and expression handling part.

  • The interface part is the calculator you see, including the title bar, the middle output box, and the keys.
  • Event handling is the handling of corresponding mouse and keyboard events.
  • The expression processing part deals with the entire input string and returns the result of the calculation, which also supports the error judgment function.

2. New Project

Select Widgets Application.

Give a name.

Generally only MinGW is required.

Default here, name can be changed

2. Interface

(1) Key

If you press a key, you can basically change it. There are three main ways to change the layout, color and font.
First open the.ui file:

A. Add a Grid Layout and resize it.

b. Drag Push Button as the key and select Expanding for both the horizontal and vertical properties of the sizePolicy property.

c. Adjust colors, set styleSheet and font

Here is the author's reference style:

border:1px groove rgb(220,220,220);
background-color:rgb(243,243,243);

Typeface:

Here you can adjust it to your personal preferences.

d. Copy the button to make a good result

e. Change Content

This not only changes the characters inside, but also the corresponding object names.

Fine tune each key again, including size, font and color, to make the overall effect better.

Numbers should be marked with a "bold" effect, and symbols should be marked with a "fine" point.

f. Modify the size as a whole with intervals

Adjust the interval. Pay attention to the details.

Here are the calculators that come with win10:

See the interval?

The author wants this effect.

You can run it first.

The spacing between the two sides will be adjusted to fit the size of the widget.

(2) Output box

The output box is simply a QLineEdit.

a. Add QLineEdit

b. Size and set the background color

Author's qss:

border:0px groove rgb(243,243,243);
background-color:rgb(245,245,245);

c. Set font, read-only, aligned

(3) Title bar

The title bar is actually very simple, a QBoxLayout

a. New Horizontal Layout

b. Add details

QLabel enters a title, and two QPushButton s indicate minimization and closure, adding two Spacer s at the same time, leaving some distance between the title and the left.

It's actually mimicking the effect of win10's title bar

You don't want to maximize it here. Because it involves the rearrangement of buttons, you can choose to do it yourself.

(4) Overall treatment

a. Title bar

Move the title bar you did in the previous step to the appropriate location and delete the included QMenuBar,QToolBar,QStatusBar.

b. Adjust overall size while adding transparency

After adjusting, it's probably the same. Transparency here is 0.9.

Perfect!

3. Event handling

(1) Cursor events

A. Title bar

a. Dragging effect

First remove the original title bar.

setWindowFlags(windowFlags() | Qt::FramelessWindowHint);

Then add the mouse listening function in protected:

void mousePressEvent(QMouseEvent *);
void mouseMoveEvent(QMouseEvent *);

Add two QPoint s to the private member. Indicate the coordinates of the current window and cursor, respectively.

QPoint mousePoint;
QPoint windowPoint;

The first function is triggered when the mouse is pressed to determine if the left button is the event->button(). If yes, get the mouse coordinates and set the window coordinates.

When the second function is triggered, the window is moved using MainWindow's move method to determine whether to hold down the left button.

Event->globalPos() takes coordinates, subtracts the coordinates of the original cursor, and adds the change to the original coordinates.

void MainWindow::mousePressEvent(QMouseEvent *event)
{
    if(event->button() == Qt::LeftButton)
    {
        mousePoint = event->globalPos();
        windowPoint = frameGeometry().topLeft();
    }
}

void MainWindow::mouseMoveEvent(QMouseEvent *event)
{
    if(event->buttons() & Qt::LeftButton)
    {
        move(windowPoint + event->globalPos() - mousePoint);
    }
}

b. Minimize and close

Take Minimization as an example, and close it as well. Change the function call.
Right-click Go to slot in the Minimize button:

Select clicked()

Add a minimization function:

Here are the closed functions:

B. Keys

The mouse event for a key consists of two:

  • Cursor move-in and move-out events, adding shadows to keys, darkening colors, etc.
  • Click the event to add or subtract characters in the output box

a. Move-in and Move-out events

This is done through an event filter. Add an eventFilter() function

 bool eventFilter(QObject *,QEvent *);

The event type is first determined by event->type(), if the cursor is hovered, then the corresponding objects are judged to increase the shadow effect.

addNumButtonEffet():

void MainWindow::addNumButtonEffect(QPushButton *button,QGraphicsDropShadowEffect *shadow)
{
    shadow->setEnabled(true);
    button->setStyleSheet(
        "border:1px groove rgb(220,220,220);"
        "background-color:rgb(193,193,193);"
    );
}

Here QGraphics DropShadowEffect *shadow is initialized beforehand.

Then add an event filter:

Here you can compare whether there are shadows:

No shadows:

Add shadows:

Uh.... This may be a problem with the screenshot tool. It doesn't seem to have much effect, but there's a big difference directly on the machine. It's better to add a shadow.

b. Click Events

Clicking an event means clicking a key and the user can see the corresponding response in the output box.

Select keys in turn, right-click Go to slot:

Select clicked()

Then add a processing function, where the author implements a function to add text and clear focus. Adding text means adding the corresponding key to the output box after being clicked by the cursor, as to why the focus should be cleared...

Because...

Because of spaces.

Because of the author's "good" habit, it is customary to add spaces before and after operators

Keep the focus on this button after clicking. By default, typing a space on the keyboard will "press" this button for you, so if you don't clear the focus, click a button on the cursor, such as 7. Pressing a space will output 7 on the output box. After the cursor clicks 8, pressing a space will output 8 on the output box.

Also note the default prompting 0 when adding text here.

void MainWindow::appendText(const QString &s)
{
    if(ui->box->text() == "0")
        ui->box->setText(s);
    else
        ui->box->setText(ui->box->text()+s);
}

void MainWindow::appendTextAndClearFocus(QPushButton *button, const QString &s)
{
    appendText(s);
    button->clearFocus();
}

(2) Keyboard events

Keyboard events deal with shadows and output box additions when keys are pressed.
Keyboard events are handled by two functions:

void keyPressEvent(QKeyEvent *);
void keyReleaseEvent(QKeyEvent *);

The first triggers when the key is pressed, and the second triggers when the key is released.

A. Add Shadow

Add a shadow and color enhancement effect when the key is pressed.

Determine each key in turn by event->key().

Keys can see here

Then add in keyRealeseEvent() to remove the corresponding shadow:

void MainWindow::keyReleaseEvent(QKeyEvent *event)
{
    switch (event->key())
    {
        case Qt::Key_0:
        case Qt::Key_1:
        case Qt::Key_2:
        case Qt::Key_3:
        case Qt::Key_4:
        case Qt::Key_5:
        case Qt::Key_6:
        case Qt::Key_7:
        case Qt::Key_8:
        case Qt::Key_9:
        case Qt::Key_Plus:
        case Qt::Key_Minus:
        case Qt::Key_Asterisk:
        case Qt::Key_Slash:
        case Qt::Key_AsciiCircum:
        case Qt::Key_Percent:
        case Qt::Key_ParenLeft:
        case Qt::Key_ParenRight:
        case Qt::Key_BraceLeft:
        case Qt::Key_BraceRight:
        case Qt::Key_BracketLeft:
        case Qt::Key_BracketRight:
        case Qt::Key_Backspace:
        case Qt::Key_Space:
        case Qt::Key_Period:
        case Qt::Key_Escape:
        case Qt::Key_Equal:
        case Qt::Key_Return:
            removeNumButtonEffect(ui->num0,num0_shadow);
            removeNumButtonEffect(ui->num1,num1_shadow);
            removeNumButtonEffect(ui->num2,num2_shadow);
            removeNumButtonEffect(ui->num3,num3_shadow);
            removeNumButtonEffect(ui->num4,num4_shadow);
            removeNumButtonEffect(ui->num5,num5_shadow);
            removeNumButtonEffect(ui->num6,num6_shadow);
            removeNumButtonEffect(ui->num7,num7_shadow);
            removeNumButtonEffect(ui->num8,num8_shadow);
            removeNumButtonEffect(ui->num9,num9_shadow);
            removeSignButtonEffect(ui->plus,plus_shadow);
            removeSignButtonEffect(ui->minus,minus_shadow);
            removeSignButtonEffect(ui->mutiply,mutiply_shadow);
            removeSignButtonEffect(ui->divide,divide_shadow);
            removeSignButtonEffect(ui->pow,pow_shadow);
            removeSignButtonEffect(ui->percent,percent_shadow);
            removeSignButtonEffect(ui->parentheses,parentheses_shadow);
            removeSignButtonEffect(ui->parentheses,parentheses_shadow);
            removeSignButtonEffect(ui->brace,brace_shadow);
            removeSignButtonEffect(ui->brace,brace_shadow);
            removeSignButtonEffect(ui->bracket,bracket_shadow);
            removeSignButtonEffect(ui->bracket,bracket_shadow);
            removeSignButtonEffect(ui->backspace,backspace_shadow);
            removeSignButtonEffect(ui->blank,space_shadow);
            removeSignButtonEffect(ui->dot,dot_shadow);
            removeSignButtonEffect(ui->C,c_shadow);
            removeSignButtonEffect(ui->equal,equal_shadow);
            break;
    }
}

There is no key to judge because it is possible to press multiple keys at the same time, and then release them at the same time to find that one key still has a shadow, so all keys are shaded when one key is released.

B. Add Output

Add output to the output box and call a function:

4. Re-processing of overall details

(1) fade-in effect

See the effect:

Qt's animation is actually used here, animation for transparency changes.

void MainWindow::fadeIn(void)
{
    QPropertyAnimation * changeOpacity = new QPropertyAnimation(this,"windowOpacity");
    changeOpacity->setStartValue(0);
    changeOpacity->setEndValue(0.91);
    changeOpacity->setDuration(2500);
    changeOpacity->start();
}

The first line represents the change in transparency, the second and third lines set the start and end values, the next set the animation time (units ms), and then the animation begins.

(2) Setting fixed dimensions

You may not set the maximum size here, but you must set the minimum size.

Setting this actually prohibits dragging to change size.

(3) fade out effect

Fade effect is similar to fade effect.

The fadeOut() function cannot be called directly before exit(0) because the animation will start on another thread, so the main thread needs to sleep for a specified number of seconds, wait for the fade out effect to complete, and then call exit(0) again.

void MainWindow::fadeOut(void)
{
    QPropertyAnimation * changeOpacity = new QPropertyAnimation(this,"windowOpacity");
    changeOpacity->setStartValue(0.9);
    changeOpacity->setEndValue(0);
    changeOpacity->setDuration(2500);
    changeOpacity->start();

    QTime start = QTime::currentTime().addMSecs(2500);
    while(QTime::currentTime() < start)
        QCoreApplication::processEvents(QEventLoop::AllEvents, 100);
}

Where addMSecs() denotes the number of seconds to delay, while the body of the while loop denotes the processing of events for this thread, where 100 denotes processing events up to 100ms and returning this statement.

No fading effect pictures will be shown here.

5. Expression handling

Since this is the entire string that is input as an expression, it needs to be judged before it is calculated.

This uses a new console project, which will be integrated later.

(1) Judgment

Use a check class judgment, because there are only 10 numeric keys, add, subtract, multiply, divide, decimal point, complement, power, brackets, spaces, so you can classify into these categories for judgment.

a. Remove all spaces

void removeAllBlank(void)
{
    size_t i = 0;
    while((i = s.find(' ',i)) != string::npos)
        s.erase(i,1);
}

First remove all the blanks, avoiding subsequent judgments.

b. Classification Judgment

Classify all the characters in the expression into five categories:

  • number
  • Decimal point
  • Operational Symbols+ - */ ^%
  • Left bracket class ([{
  • Right Bracket Class)]}

It then determines for each type whether its next character is the allowable type, or returns false.

Like when you meet one (or [or {

Then its next cannot be an arithmetic symbol or decimal point, of course - and +, because
(-7) (+234)

In this case.

Then save the symbol to see if there is a right parenthesis behind it.

if(isLeftBrace(i))
{
    if(isSignOrDot(i+1))
    {
        if(s[i+1] != '-' && s[i+1] != '+')
            return false;
    }
    braces.push(s[i]);
}

The whole judgement function is as follows:

bool valid(void)
{
    if(isSignOrDot(0) || isRightBrace(0))
        return false;
    len = s.size();
    stack<char> braces;
    for(size_t i=0;i<len;++i)
    {
        if(isLeftBrace(i))
        {
            if(isSignOrDot(i+1))
            {
                if(s[i+1] != '-' && s[i+1] != '+')
                    return false;
            }
            if(isRightBrace(i+1))
                return false;
            braces.push(s[i]);
        }
        else if(isRightBrace(i))
        {
            if(isDot(i+1) || isDigit(i+1) || isLeftBrace(i+1))
                return false;
            if(isRightBrace(i+1))
            {
                stack<char> braces_copy(braces);
                if(braces_copy.empty())
                    return false;
                braces_copy.pop();
                if(braces_copy.empty())
                    return false;
            }
            if(braces.empty())
                return false;
            char brace = braces.top();
            if((brace == '(' && s[i] != ')') || (brace == '[' && s[i] != ']') || (brace == '{' && s[i] != '}'))
                return false;
            braces.pop();
        }
        else if(isSign(i))
        {
            if(isSign(i+1) || isDot(i+1) || isRightBrace(i+1))
                return false;
        }
        else if(isDot(i))
        {
            if(isSignOrDot(i+1) || isBrace(i+1))
                return false;
        }
        else if(isDigit(i))
        {
            if(isRightBrace(i+1))
            {
                if(braces.empty())
                    return false;
                char brace = braces.top();
                if((brace == '(' && s[i+1] != ')') || (brace == '[' && s[i+1] != ']') || (brace == '{' && s[i+1] != '}'))
                    return false;
            }
        }
    }
    return braces.empty();
}

It is important to note that when right brackets are encountered, in addition to determining whether they are separate right brackets, there is also a decision whether they match the previous left bracket.

c.plus 0

This is for the singular operator-case, such as (-7), and then converts it to (0-7):

string getResult(void)
{
    size_t len = s.size();
    for(size_t i = 0;i<len;++i)
    {
        if(s[i] == '(' && (s[i+1] == '-' || s[i+1] == '+'))
            s.insert(i+1,"0");
    }
    return s;
}

After the left parenthesis, determine whether it is - or +, and insert 0 if it is.

(2) Calculation

a.calc auxiliary class

Two stacks, operator stack and operand stack, are used in the calc auxiliary class.

private:
    stack<char> operators;
    stack<double> operands;

There are two important methods:

bool canCalculate(char sign);
void calculate(void);

The first method takes the next symbol to enter as a parameter to determine if the first two numbers of the operand stack can be calculated, and if possible, the second function.

calculate() takes two operands out of the stack and an operator, and pushes them back into the stack after the result is obtained.

void calculate(void)
{
    double post = popAndGetNum();
    char sign = popAndGetSign();
    double pre = popAndGetNum();
    double result = 0.0;
    switch (sign)
    {
        case '+':
            result = pre+post;
        break;
        case '-':
            result = pre-post;
        break;
        case '*':
            result = pre*post;
        break;
        case '/':
            if(fabs(post) < 1e-6)
            {
                cout<<"Error.Divisor is 0.";
                exit(EXIT_FAILURE);
            }
            else
                result = pre / post;
        break;
        case '^':
            result = pow(pre,post);
        break;
        case '%':
            result = static_cast<int>(pre) % static_cast<int>(post);
        break;
    }
    push(result);
}

bool canCalculate(char sign)
{
    if(sign == '(' || sign == '[' || sign == '{' || operators.empty())
        return false;
    char t = getSign();
    if(t == '^')
        return true;
    switch (t)
    {
        case '+':
        case '-':
            return sign == '+' || sign == '-';
        case '*':
        case '/':
        case '%':
            return sign == '+' || sign == '-' || sign == '*' || sign == '/' || sign == '%';
    }
    return false;
}

The following is the calc class:

class calc
{
private:
    stack<char> operators;
    stack<double> operands;

    char getSign(void)
    {
        return operators.top();
    }

    double getNum(void)
    {
        return operands.top();
    }

    void popSign(void)
    {
        operators.pop();
    }

    void popNum(void)
    {
        operands.pop();
    }

    double popAndGetNum(void)
    {
        double num = getNum();
        popNum();
        return num;
    }

    char popAndGetSign(void)
    {
        char sign = getSign();
        popSign();
        return sign;
    }
public:
    void push(double num)
    {
        operands.push(num);
    }

    void push(char sign)
    {
        operators.push(sign);
    }

    char get(void)
    {
        return getSign();
    }

    void pop(void)
    {
        popSign();
    }

    double result(void)
    {
        return getNum();
    }

    void calculate(void)
    {
        double post = popAndGetNum();
        char sign = popAndGetSign();
        double pre = popAndGetNum();
        double result = 0.0;
        switch (sign)
        {
            case '+':
                result = pre+post;
            break;
            case '-':
                result = pre-post;
            break;
            case '*':
                result = pre*post;
            break;
            case '/':
                if(fabs(post) < 1e-6)
                {
                    cout<<"Error.Divisor is 0.";
                    exit(EXIT_FAILURE);
                }
                else
                    result = pre / post;
            break;
            case '^':
                result = pow(pre,post);
            break;
            case '%':
                result = static_cast<int>(pre) % static_cast<int>(post);
            break;
        }
        push(result);
    }

    bool canCalculate(char sign)
    {
        if(sign == '(' || sign == '[' || sign == '{' || operators.empty())
            return false;
        char t = getSign();
        if(t == '^')
            return true;
        switch (t)
        {
            case '+':
            case '-':
                return sign == '+' || sign == '-';
            case '*':
            case '/':
            case '%':
                return sign == '+' || sign == '-' || sign == '*' || sign == '/' || sign == '%';
        }
        return false;
    }

    bool empty(void)
    {
        return operators.empty();
    }
};

private encapsulates some simple tool methods for manipulating two stacks. Public pop() and get() are operations on the operator stack. Because there is no need to manipulate the operand stack externally, calculate() operates on it. Public push is overloaded and can be pushed to the operand stack or operator stack.

b. Calculation section

The calculation section is placed directly in the main here:

int main(void)
{
    check chk;
    while(!chk.inputAndCheck())
        cout<<"Error!Please input again.\n";
    string s = chk.getResult();
    size_t len = s.size();
    calc c;
    for(size_t i=0;i<len;++i)
    {
        if(isdigit(s[i]))
        {
            double num;
            size_t i1 = i+1;
            while(i1 < len && (isdigit(s[i1]) || s[i1] == '.'))
                ++i1;
            istringstream input(s.substr(i,i1));
            input>>num;
            i = i1-1;
            c.push(num);
        }
        else if(s[i] == '}' || s[i] == ']' || s[i] == ')')
        {
            char sign;
            char start = (s[i] == '}' ? '{' : ( s[i] == ']' ? '[' : '('));
            while((sign = c.get()) != start)
                c.calculate();
            c.pop();
        }
        else                          //s[i]  is  [ ( {  + - * / ^ %
        {
            while(c.canCalculate(s[i]))
                c.calculate();
            c.push(s[i]);
        }
    }
    while(!c.empty())
        c.calculate();
    cout<<"result is "<<c.result()<<endl;
    return 0;
}

Each character of the expression is processed one by one. If it is a number, it is extracted and stacked.
For the right bracket class, keep extracting from the operator stack until the expression within this bracket is evaluated.

Otherwise, if it is a left parenthesis or an operator, it is always calculated when it can be calculated, two operand operations are extracted and stacked, and the new operators are stacked.

Finally, use result() to get the result.

c. Testing

Here are a few very long examples.

Of course the author tested a lot of examples

6.6/{2.3+34.3*2.22-5%2+22%4*[2+3.4/5-(4.3+3.2*33.3)]+34.3} + 7.8*{2.4-6/6+0-0*[23.4-3.4/6+4*(2.2+3)]}+0 - 0 + 0.0 
= 10.8569

3.4 - (+3.34) + 34.3 * (-2) / 3.34 + {[(-3.4)^2/3.4+3.4/3]-3.32+[3*(-3)]}
= -28.2656

9^5-34.4^2.3+5%6-34+66%78-78%4 + (-3)*3.4 / {3*(-7)+[3*(-8)+3*(3.4+4.34)/9.3-3.2 + 0.0 - 0]+0.0 - 0}+3.4^4/6.888 
= 55683.2

If you don't believe it, you can calculate it manually.

6. Integration

This part integrates the interface part with the expression processing part.

(1) Set up the calling process of the interface and get the output results

The program that evaluates the expression is called MyCalc.exe. Note that it is placed under the corresponding project folder and called using QProcess.

Executed with execute, the expression first removes all spaces, then passes them to the calculator as a command-line parameter, and then the calculator writes the result of the calculation to the result.txt file, which Qt reads. If reading # indicates that the expression is entered incorrectly, otherwise, the correct result of the calculation is obtained.

For results because the fixed format is set in the calculator, for

1+2

Will also return

3.000000

In this step, remove the extra zero and pay attention to the decimal point.

(2) Modify some details

a. Mouse keyboard modification events

Modify the setText to pass the results to it.

Format numbers in b.exe

Set the fixed format, otherwise it will show scientific counting, and set precision if decimal is required.

c. Set up error alerts

When an error occurs here, the output'#', and then the main program reads it, it will prompt "Expression error, please re-enter."

There are also incorrect hints for dividing by zero, which should be noted:

d. Consider integrating error handling

For example, if you enter a point, you cannot continue to enter a point, if you enter a multiplication or division sign, you cannot continue to enter another symbol:

7. Package and Publish

(1) Download Enigma Virtual Box first

(2) Add environment variables

Add the bin shown below the Qt folder to the Path environment variable.

(3) Packaging library files

Package using windeployqt, first set the program to release mode, run once, generate the release exe, then copy the EXE to a separate folder, then use the command line to enter the folder, run

windelpoyqt xxx.exe

This command copies the required dll to the current folder.

(4) Generate a single exe

Open Enigma Virtual Box, select

The first chooses the release exe, the second chooses the packaged folder, then chooses Add Files, chooses Add Recursively, and adds all the files (folders) generated in the previous step.

Select the compressed file here.
Then select Compression to wait for completion.

(5) Testing

Click Run.

Be accomplished!!

8. Source Code

9. Reference Links

1.Qt fade in

2.Qt key

3.Qt Title Bar

4.Event Filter

5.Qt Mouse Event

6.Qt Delay Processing

7.Qt File Read-Write

8.Qt packaged into single file

10. Last

This simple calculator has a lot of room for improvement, such as adding a variety of "numbers":
Sine Function, Cosine Function, Tangent Function, Arcsine Function, Exponential Function, Logarithmic Function, Higher Derivative, Abstract Function, Composite Function.

In addition, you can improve the button of the rectangle by changing it to a rounded rectangle or an ellipse.
In addition, fade-in and fade-out effects can be added for shading.

And finally there's grinding. Because win10 has a grinding effect, this author won't yet....
Finally, let's take a look at the results (because the size of the motion map is limited to a simple expression...):

Hope you also have a calculator of your own!

Posted by 00009261 on Mon, 18 Nov 2019 21:07:22 -0800