python+tkinter仿制win10标准型计算器(中文版)完美教学

前言

马上就要实习了,而我却没有拿得出手的项目..哎实力不够啊。还有六个月时间,先从简单开始吧,一步一个脚印,大家一起加油!

“设计很难,模仿不易。”这是我创作计算器时内心的独白。设计出一款产品固然很难,但是你要模仿他设计出类似的一款产品也不容易,你必须要知道它底层实现的逻辑原理。

                  

设计前,天真的我以为,仿造个标准计算器,不就加减乘除平方开根号么这有什么难的?给我一天时间足够啦!确实,实现主干代码并不难,一天确实能完成。但是仅考虑常规计算思路是远远不够的,这样会遗留下诸多bug,如 运算符与开根号求倒数等进行交叉运算后显示结果与预期结果不相符情况等等很多,这也因此让我花了一个多星期的时间才算是让各个运算逻辑相互兼容,尽可能的修复已知BUG。修复BUG过程你必须要假想自己是一个变态老板,就是不按常规思路计算客户的账单,乱点一通。因此,你必须要预判出所有可能出现的情况,我愿称这项目为面向变态老板设计。

前阵子学校附近有疫情,然后学校就急匆匆的上完课就放假了,本想完成全部代码后赶在2021发布博客的,可终究没有实现。家人又叫我去那边帮忙,导致我设计时断时续,很不巧tkinter我也是自学没多久,不够熟练,这次写代码的时候贪快不愿意写备注,想着下次回来看动动脑想下还能加深印象。结果,过几天回来一看哪搭哪都不知道,真的一头雾水,所以又不得不重新找下资料看..这时间就是白白浪费呀,其实你提前备注好也是等价找资料的,所以说,写完代码马上备注很重要。本来连贯的思路经过几天工作的洗礼就没了,你说又要重拾起来,那可得花好一段时间,特别是代码长了之后。所以说我们最好能够完成一件事再干另外一件事。

在设计过程中,我遇到了诸多烦心的地方,如,如何提高代码可读性和使用效率?该先从何入手?是先一个模块一个模块设计还是先规划好所有模块功能再单独拎出来一个一个设计呢?对于刚开始没啥经验的我来说当然是想着走一步算一步,正因如此,我没有考虑整体问题在后面设计过程中导致很多地方出现运算不兼容,显示有误的情况,查错起来那才叫难受,真的是好多时候都想放弃..幸好最后挺过来了,现在回想起来真不容易。还有就是代码,如果可以的话先分析记录一下各个功能实现的逻辑,最好能找出各个功能的共性,然后设计对应的方法,每执行一个功能,就调用那个方法,还有在if条件判断的时候,我们这个计算器有24个按钮,哪些按钮功相似可以放在一起,哪些按钮执行完后就不用再执行下面的语句?考虑好这些情况能让我们提高一定的写代码效率。还有一个就是命名规范,我英语不好,遇到想写又不会的单词就直接百度翻译,目的就是能让大家更好的理解我实现每一步的思路。有时候遇到问题死磕好久找不出来,可以试试找一下你身边的小伙伴,让他来调试一下两下,说不定就能找出问题出现的原因了。

其他也没啥了,我们进入正题吧。

这款计算器部分显示是中文。在这我希望我们国产软件能够越来越好,也为国产标志的软件尽自己一点绵薄之力。

目录

前言

软件介绍

软件缺陷

软件设计

1.设计思路

1.1显示部分

1.2触控部分

1.3思路图解

2.详细步骤

2.1创建窗口对象

2.2创建显示框架

2.3放置显示屏

2.4创建按键框架

2.5放置按钮 

2.6★定义按钮事件

2.7生成可执行文件

3.完整代码

想说的话


软件介绍

这是一款中文双显计算器,支持主流运算,功能细节上能与win10自带的标准型计算器有百分之60以上的相似度,精度上支持六位小数(可自行修改)。

软件缺陷

1.界面不够美观

2.不能对小数进行开根号

3.不支持键盘输入

4.出现警告框后按钮不会弹起

5. "%", "CE", "清零"三个按钮功能等价 (目前已修复“CE”情况)

提到 "%", "CE" 我真是头疼,百度琢磨了半天也没理解这两个符号表达是什么意思,感觉他们说的win10计算器跟我的win10计算器都不是同一个..因为研究这两个按键花费了不少时间没研究出来,又感觉这也不经常用,我索性就把他们归到清零的功能上了,而我在win10计算器输入数字后点他们确实也是清零了。(CE我会了)

软件设计

1.设计思路

计算器分成两个部分——显示和触控。

1.1显示部分

显示部分分为上显示屏和下显示屏。下显示屏是表示每一步计算的数值,也只显示数值,上显示屏表示的是计算过程。理论上,下屏应为单行显示,可复制,可通过键盘输入数字及运算符(该功能尚未完成),因此,我们选择的控件应为entry,其功能与text相似,而上屏的作用是显示计算过程,里边的元素不可复制,因此上屏选用的控件为label。我们知道,label不能直接更改数据,所以这里引用了可变追踪对象StringVar(),通过改变这个对象的值来改变label显示的值,同样的下层entry控件也需要这个可变对象,因为我们计算操作是使用鼠标点击按钮来实现计算,而不是通过键盘输入数字来计算。

1.2触控部分

这一部分是整个组件的核心。我们需要对按钮绑定事件,以保证每次点击按钮能触发对应的事件。24个按钮里边无非就4种类型 数字(0~9),运算符(加减乘除等于),算法(求倒数求平方开根号相反数)以及回退操作(删除初始化)。在此,我们必须要设定一些变量保存数据,方便我们计算。我设置了四个变量,factor1,  factor2, operator, last_click用于记录因子1,因子2,运算符,以及上一次点击。为了简化计算我是直接使用了eval函数和pow函数,如果对这两个函数算法的实现思路感兴趣的朋友可以了解一下我这两篇文章。

python pow函数——幂运算 快速幂算法实现思路_lishuaigell的博客-CSDN博客说明python 内置pow函数用于实现幂的运算,在这里我使用的是快速幂算法实现pow函数功能。快速幂快速幂算法本质上基于的是分治思想。优点:其时间复杂度为 O (log₂N), 与暴力遍历时间复杂度O (N)相比效率有了质的提高。待完善之处:指数暂支持输入整数。思路不断将高次幂拆分成低次幂,直到低次幂无法再拆分为止。而此时低次幂的值就显而易见了,就是底数(1次幂)。然后通过最低次幂(1次幂)不断往上求取更高次幂的值,最终得出结果。我以2的10次方为例演示一实现思b路。https://blog.csdn.net/lishuaigell/article/details/122474662

python eval函数——求解字符串表达式 算法实现思路_lishuaigell的博客-CSDN博客说明应对不同情况下的字符串表达式求值,本篇提供双栈和单栈两种解法。当表达式较为简单不存在括号时,可使用单栈求解,其他情况使用双栈。算法一、单栈当表达式不存在括号时,如"3+2*5-4/2"可使用单栈求解。思路我们遍历表达式,每遇到一个运算符就对运算符前面的数字进行相应操作。定义了一个栈,将消除乘除号后的数字添进栈中,最后使用sum方法将栈内元素求和得出最终结果。当遇到"+"号时,将符号前面的数字(称为num)压入栈中当遇到"-"号时,将(-num)压入栈中当遇到"*"https://blog.csdn.net/lishuaigell/article/details/122114239

记住,在这里你必须要知道每个按钮间都不存在互斥现象,千万不要想当然的以为点了这个按钮之后就不会点击那个按钮,你必须要料想所有可能出现的结果。

1.3思路图解

从开始设计窗口到设计按钮框架大致的思路可以用下面的gif表示

 你们看一下这个思路图,有些地方我在下面做出了相应的解释。

解释:

 看到“删除”的分支语句,删除一个字符指的是删除下显示屏的一个字符。

看到“加减乘除”的分支语句,重显示的意思就是此时下显示屏显示的值是虚值(即运算结果),当下一次输入数字的时候显示的值会被覆盖掉。

记录前面输入的数字:就是获取此时下显示屏的值。因为运算符不会出现在下显示屏,下显示屏只会记录数字,每出现一次运算符后将会读取一次数据之后下显示屏会处于重显示状态。

直接开平方: 因为我们计算是读取下显示屏的数据,在读取前下方数据如何改变逻辑上都是行得通的。

我以下方gif为例简单讲一下实现过程。

下方显示屏初始值是0, 输入数字后,如果第一个字符是0则需要将其覆盖,覆盖后下方显示为3。点击乘号后,因子factor1记录下方显示屏的值(3),运算符operator记录此时点击的运算符(*)。上方显示屏显示 因子1和运算符,下方显示屏处于重显示状态。当再次输入数字5的时候,5覆盖掉3的值,下方显示屏显示5,点击运算符+号后,因为因子1存在了,5的值赋值给因子2。然后就使用eval函数对“因子1 运算符 因子2”这个表达式进行求解,将的出来的结果(15)赋值给因子1,上方显示屏继续显示 因子1和运算符,下方显示屏再回到重显示状态。点击6,下方显示为6,再点击等号。用因子2记录下方显示屏的值6,然后再用eval对“因子1 运算符 因子2”表达式进行求解,求解后上方显示计算过程,下方继续处于重显示状态。

看到这里细心的朋友会发现我前面说这些按钮只有四种类型,但是在上方的思路图中却出现了不止四条的分支,也就是出现了不止四种情况,这是为什么呢?我只能说个人习惯吧,一开始设计的时候我就把他们分开了,所以现在写的时候也这么写了,但是存在个别按钮(等号=),不分开我也要将它分开,或许我将它搞得太特别了。至于为什么,在详细步骤那里我再好好跟大伙说说原因。

其实,上方思路图只展示了常规使用计算器的思路,只考虑上方几种情况是远远不够的,为保证计算器稳定性我们必须考虑更多情形,即,非常规情况

2.详细步骤

2.1创建窗口对象

首先定义类TkWindows,之后导入tkinter包,在类的init方法里边创建窗口对象,以及设定好尺寸,名称等参数。尺寸的话我根据win10计算器设计的,虽然还是有差别,凑合用吧,设置窗体尺寸和按钮框架显示框架的尺寸我也花了不少时间。设置完尺寸后要设置最大拉伸长度,因为拉伸后那些按钮和框架不会跟着拉伸,还有就是设定窗口名了。

代码如下:

import tkinter
class TkWindows:

    def __init__(self):
        # 创建窗口对象
        self.win = tkinter.Tk()
        # 设置窗口宽度,高度以及窗口初始位置 x 为小写字母x 第一个加号后面参数是x轴坐标,第二个参数是y轴坐标
        self.win.geometry("360x460+570+200")
        # 定义拉伸的最大尺寸
        self.win.maxsize(width=360, height=460)
        # 定义窗口名称
        self.win.title("李帅哥的标准型计算器")

设计思路部分我已经大概提过有哪些变量,哪些控件了,因此先提前在init里边创建好。

        # 初始化数据
        self.lower_display = None  # 上显示屏
        self.upper_display = None  # 下显示屏
        self.entry = None  
        self.label = None

        self.operator = ""  # 运算符
        self.factor1 = ""  
        self.factor2 = ""
        self.last_click = ""  # 上一次点击

然后进入消息循环

        # 进入消息循环
        self.win = tkinter.mainloop()

 在主函数调用该类

if __name__ == '__main__':
    TkWindows()

点击运行,就能出现框体了

2.2创建显示框架

定义方法text_frame,创建一个frame框架text_frame,将该框架放置到win窗口中的上部分。

    # 显示屏——双显
    def text_frame(self):
        text_frame = tkinter.Frame(self.win, width=360, height=190)
        text_frame.pack(side="top")

 frame 第一个参数为要放置的对象,第二个参数是宽度,应与窗口宽度一致,第三个参数是高度。

然后将该方法添加到init里面,就能生成这个框架了。

self.button_frame()

2.3放置显示屏

上显示屏使用的控件是label,下显示屏使用的控件是entry。它们都有一个参数叫textvariable,是用来接收可变对象,说白了就是跟可变对象绑定后,可变对象的值发生改变,显示框的值也发生改变。常见能接收的类型有StringVar() 和 IntVar()。因为我们计算是使用eval函数,里边的表达式是字符串类型的,所以我们的接收类型就使用了StringVar(),其常用的方法有get方法--获取当前显示框的文本,和set方法--设置显示框的值。之后我们会大量使用这两个方法,是不是很简单粗暴?因此在text_frame方法里面添加如下语句。

        # 定义上下层显示屏可变追踪对象用于及时更新数据
        self.lower_display = tkinter.StringVar()
        self.upper_display = tkinter.StringVar()
        # 设定下层显示的初始值
        self.lower_display.set("0")

 添加完后我们下显示屏默认会显示0,而上显示屏默认显示。

 创建上显示屏label

# 设定上层显示组件, label——可多行显示,不可复制,不可直接通过键盘更改文本内容
        self.label = tkinter.Label(text_frame, font=("loMa", 14),
                                   textvariable=self.upper_display, bd=0, anchor="se", bg="LightGray")
  • text_frame: label要放置的对象。我们要把label放置在text_frame框架中。
  • font:字体。里边第一个参数是字体样式,第二个参数是字体大小。
  • textvariable:绑定可变对象。这里我们绑定的是上显示屏,其类型是StringVar()。
  • bd:边框大小。因为我们上下显示屏要看成一个整体,所以不能有边框,这里设置为0。
  • anchor:图形放置的位置。"se"表示东南部。相当于放在右下角了。说到这东西我必须吐槽一下,label明明也有justify属性(对齐方式),我把它修改为向右对齐却一点用都没有还是会居中显示,而entry修改这个属性后又能向右对齐,这给我整无语了。
  • bg:背景色。我为什么用这个颜色呢,因为我win10计算器是在pycharm下打开的,刚好那时候就显示这个色,我就把计算器调成这个颜色了,结果win10计算器颜色会变..想改其他颜色可以参考一下这个老哥@笑待人生原创库 的图片。

另外,要想查看更多tkinter命令可以查阅一下这篇文章。

Python GUI 编程(Tkinter) | 菜鸟教程

 创建下显示屏entry

        # 设定下层显示组件, entry———单行输入显示,可复制,可直接通过键盘更改文本内容
        self.entry = tkinter.Entry(text_frame, font=("loMa", 34, "bold"),
                                   textvariable=self.lower_display, bd=0,
                                   cursor="circle", justify="right", bg="LightGray")

这里的参数与label大致相似不再过多解释,在font参数那里多加了“bold”参数,表示加粗字体。下方参数多了个cursor表示鼠标图标,当我们将鼠标放上下显示屏的时候他会变成圆圈。截图截不了鼠标,各位可以自己试下,还是挺好看的。

放置显示屏

创建好显示屏后就要使用布局管理器将他们放入显示框架里面了,使用哪种布局呢?pack?虽然pack很方便,但是这里用不得pack。因为entry没有高度height属性,如果直接使用pack放置的话entry的高度会使用默认高度,很可能达不到你想要的结果。而place布局可以克服这一点,他可以自定义控件初始位置和大小,因此我们选用place布局管理器对上下显示屏进行布局。虽然label有高度属性,但是我们知道,布局管理器在同一容器里面只能使用一种,否则会报错,所以我们就统一使用place布局,并在里边设置起始位置和大小。

        # 设定组件的放置位置及大小, entry没有高度这个属性,而默认高度显示不够美观,因为place布局可以自定义组件的高宽度,所以采用place布局
        self.label.place(x=1, y=10, width=359, height=70)
        self.entry.place(x=1, y=50, width=359, height=120)

 x轴方向稍微空出一个像素会显得柔和些,不会那么突兀。

2.4创建按键框架

定义button_frame方法以及button_frame框架,将其放入到win窗口中的下部分。

    # 按键
    def button_frame(self):
        button_frame = tkinter.Frame(self.win, width=370, height=270)
        button_frame.pack(side="bottom")

 将方法添加到init里面

self.text_frame()

2.5放置按钮 

思路

遍历排列好的按钮字符串,定义行变量和列变量记录该按钮的行列值,列的值每满4个就要清零,行的值就要加一。然后使用grid布局将这些按钮根据此时行列值放置到按钮框架里面。放置的过程同时要绑定事件。事件下一小节会讲,这里就听一下概念就好。

确定按键顺序

我们根据计算器按键的顺序用一个按钮字符串存储起来,因为我们使用了中文字符,有时候一个按键的长度不为1,为了识别长度大于1的情况,我就用了花括号将按钮长度大于1的按钮名括起来。遍历时就能通过识别左右括号来确定里面按钮的值了。而长度为1的按钮直接添加到按钮框架即可。计算器按钮的布局是典型的二维表布局,所以使用grid布局管理器。先初始化变量。

        # 设定计算器按键的顺序, 当按键名称长度大于一时用花括号括起来,方便后续判断
        button_str = "%{CE}{清零}{删除}{求倒数}{求平方}{开根号}{除/}789{乘*}456{减-}123{加+}{相反数}0{小数点.}{等于=}"
        ind = 0
        row = 0
        col = 0
        length = len(button_str)

按钮放置与事件绑定

定义st变量记录按钮名称,遍历字符串,如果遇到左花括号,则在该位置使用index寻找右花括号的索引值。找离左括号最近的右括号,所以要设置查找的起始索引。找到后右花括号的位置就能使用切片获取按钮的值了,获取完值后索引移动到右花括号的位置(因为执行完一次while索引都会加1,所以就不用移动到下一个按钮的位置)。如果没有花括号则按钮名长度就是1,直接添加即可。

设置完按钮属性后将按钮放入按钮框架中,同时对按钮绑定事件,绑定事件的bind方法里面有两个参数,第一个参数是事件动作,第二个是事件对象。按钮发生相应的事件动作后,将会调用绑定的事件对象(函数)。

常见的事件动作有

  • <Button-1> 点击鼠标左键
  • <Button-3> 点击鼠标右键
  • <KeyPress-A> 按下'A'键('A'可换其他按键)
  • <Double-Button-1>  双击鼠标左键

每次放置后行列值都要发生相应的变化。

        while ind < length:
            # 当遇到花括号时(名称长度大于一),按键名为花括号里边的元素
            if button_str[ind] == "{":
                # 找到离该左花括号最近的右花括号
                end = button_str.index('}', ind)
                st = button_str[ind + 1:end]
                # 指针移动到右花括号位置
                ind = end
            # 当名称长度为1时
            else:
                st = button_str[ind]
            # 设置按钮属性
            button = tkinter.Button(button_frame, text=st, font=("loMa", 14),
                                    width=8, height=2)
            # 按钮绑定事件click_button_event,当鼠标左键单击按钮时触发该事件
            button.bind("<Button-1>", self.click_button_event)
            # 使用grid网格布局放置按钮
            button.grid(row=row, column=col)
            col += 1
            # 每行放置四个按钮, 每满四个换一行
            if col >= 4:
                row += 1
                col = 0
            ind += 1

2.6★定义按钮事件

重点来了。因为所有的按钮都绑定了同一个事件,所以计算器的核心算法都在这个事件里面。

先定义按钮事件,事件必须带至少一个参数,这个参数默认名为event。

    # 定义按钮事件
    def click_button_event(self, event):

我们可以通过event获取按钮的相关信息如名称,位置等。这里我们必须要获取按键名称才能对各个情况进行判断。

        # 获取当前点击的按钮名称
        button_text = event.widget["text"]

我们由浅入深,先从容易理解的情况开始。

 清零

前面我们讲过,计算器其中一个缺陷就是"%", "CE", "清零"三个按钮功能等价。因为我不太明白"%", "CE"的计算逻辑,所以我直接把他俩归并到清零功能上了。(已修复CE情况,CE功能相当于将下显示屏设为0)

我们在计算器进行一些计算后点击清零会发现下显示屏变为0,上显示屏不显示,相当于回到了初始状态,所以点击清零相当于对所有数据进行了初始化。我们就定义一个初始化方法,初始化数据。(该方法在按钮事件里面定义)

        # 对各个数据初始化
        def initialize():
            self.lower_display.set("0")
            self.upper_display.set("")
            self.factor1 = ""
            self.factor2 = ""
            self.operator = ""
            self.last_click = ""

 于是第一种情况我们就考虑完成了。

如果点击的按钮名称为清零CE%其中的一个,那么我们就调用初始化方法。(代码已更新)

        if button_text in "清零CE%":
            # 当点击按钮为"CE"时,我们下显示屏清零。但如果上一次点击是等于号的话我们进入一次新运算,要进行初始化。
            if button_text == "CE" and self.last_click != "等于=":
                self.lower_display.set("0")
            else:
                initialize()

 至于为什么规定等号就进入一次新运算?可先看到下面的等号分支部分。

 删除

计算器删除功能无非就是删除前面输错数字,因此我们进行判断,如果上一次点击的是数字就对其删除,否则不作响应。特别地,当上一次点击是等号的时候,上显示屏清空值,使用set方法将上显示屏设为空就好了。删除的思路就是确定下显示屏删除后是否还有值,有值就直接删除,没有值的话就设为0。

也许有朋友会有这样的一个疑惑,进行一次计算:点击数字8,再点击+号,想输入7不就输不了了?现在下显示屏还是显示8,按照上面那个删除的思路,因为上一次点击是+号不是数字,所以删不了8....这您就别担心,我在前面的设计思路提过重显示这个词吧?现在下显示屏处于重显示状态,如果下次点击是数字的话,他会直接覆盖掉之前的数字8,所以直接点击7就可以了。

 因此按钮的第二个情况分支就是

        elif button_text == "删除":
            # 删除最后一个字符
            if self.last_click in "0123456789小数点.":
                # 分析删除一个字符后的情况
                text = self.lower_display.get()[:-1]
                if len(text) == 0:
                    self.lower_display.set("0")
                else:
                    self.lower_display.set(text)
            # 如果上一次点击是等号,则需保留下方数据,清除上方数据
            elif self.last_click == "等于=":
                self.upper_display.set("")

 记录上一次点击按钮

我们每执行完一次事件,都要记录上一次点击按钮的值,就拿删除功能而言,他也要判断上一次点击按钮是否是数字才能进行相应的操作。而有些按钮不应该被记录,这些按钮分别是“删除清零CE%”,我称这些按钮为状态按钮,因为他们只改变了计算器的状态,不会参与任何计算。

为什么要这么做呢?我打个比方,输入一个1,再输入一个1,输入第二个1的时候会判断上一个输入是否是数字,如果是的话加在它后面变成11,如果不是的话覆盖掉前面的值,变成1。显然,现在下显示屏显示是11。现在我们点击删除键,因为删除一个字符后还有值,所以现在显示变为1。此时输入一个2,如果上一次点击记录的是删除键,那么因为它不是数字,所以下显示屏会覆盖掉前面的值,显示为2,而按我们的计算思路是删除1之后继续输入数字2应该要显示12才对的,因此删除键不能参与记录上一次点击,那样的话上一次点击的按钮依旧是数字,这样就解决了数据插入的问题。

因此在事件的末尾添加如下语句。

        if button_text not in "删除清零CE%":
            self.last_click = button_text

而每执行完一次事件后下显示屏应该都要处于最新显示的状态。当输入数字的长度大于显示长度时应显示后面的数字。比如输入数字12345,而显示屏只能显示3个数,那么这时显示屏显示的数字是345。

        # 每执行完一次后将光标移动到最后面位置(考虑到有时候会通过键盘输入某些字符)
        self.entry.icursor("end")
        # 当输入的文本长度大于文本框长度时会显示更近输入的内容
        self.entry.xview("end")

虽然我没有完善键盘输入的功能,但我还是设置了每次执行完一次事件光标移动到尾部的过程,因为有时候吧,偶尔输入一两个数字,不输入其他的话还是可以的。

◆等于

这次先上代码再说明情况

        elif button_text == "等于=":
            # 出现多次点击等号时保留因子二不变,让结果与因子二计算
            if self.last_click != button_text:
                # 仅在首次点击等号时记录因子二
                self.factor2 = self.lower_display.get()
            # 正常情况直接计算结果
            if self.operator:
                expression = self.factor1 + self.operator + self.factor2
            # 非正常情况点击数字后直接点击等号,上层将显示如 "8="
            else:
                expression = self.factor2
            # 用因子一保存结果
            try:
                self.factor1 = formatting_data(eval(expression))
                self.lower_display.set(self.factor1)
                self.upper_display.set(expression + " = ")
            except ZeroDivisionError:
                tkinter.messagebox.showwarning("李帅哥温馨提示", "0 不能作为除数!")
                initialize()

思路:首次点击等号时用因子2记录下显示屏数据。一般等号是在前面输入“数字1  运算符  数字2”之后出现的,输入等号后就用因子2记录数字2。然后组合字符串因子1, 运算符, 因子2,通过eval函数求解结果。然后在上显示屏显示计算过程,下显示屏显示结果。提到上下显示屏我再说一下。下显示屏显示数值,也只显示数值。上显示屏显示运算过程。运算过程就只有两种情况“因子1  运算符”, “因子1 运算符 因子2 = 结果”。正常输入表达式的话是有结果的,但少不了特殊情况,如果你就输入一个数按等于的话,表达式就是这个数,所以结果也是这个数。如果连续点击等号的话因子二的值不会变,还是第一次点击等号时数字2的值,所以连续点击会直接让结果与因子2相加。

另外,当0作为除数求表达式是我们要给出提示,因为0作除数时是无效输入。这里我使用了警告框,是tkinter下面的一个组件,需要提前导入。

import tkinter.messagebox

 看一下提示效果。

哎,因为我这个等于号的算法设计的不够好,导致小等于号处处受到排挤,在其他分支都要额外考虑他的存在..怪难受的,都怪我。明明他跟加减乘除都是运算符,可结果只有它独自承担所有。

考虑到等号的特殊性,我们规定,每进行一次等号运算后,都将开始一轮新的计算,有时候还需要对数据进行初始化。

如何理解开始一轮新的计算?我们看一个早期BUG。

各位发现有啥不妥的地方了吗?好像没啥毛病是吧?其实这里就是细节的问题了,这里的运算符是上一次计算的运算符,如果我不要加号要减号呢?不要不行,这里必须要。也许有人会说在点根号之前点一下减号不就可以了吗?确实可以,但,我说过这是面向老板编程,老板偏不走寻常路怎么办?所以我们只能委屈自己,尽量符合他的要求,设计更加人性化一点。上面我们规定如果上一次点击是等号而下一次点击不是等号的话进入新一轮计算。那么这里我们就进行一次判断,如果对使用等号求出来的结果进行开根号时,需要初始化数据,仅保留开根号的结果,以该结果开始等待下一次计算。

那么,等于这条分支就完成了。

格式化数据formatting_data(可直接使用round函数)

在上面的代码了出现了formatting_data的字样,这是我定义的一个方法,他是用来格式化表达式的值--保留若干位小数,并去0。

思路:接收一个整型或浮点型数(eval得出来的结果是整型或浮点型),使用format方法使其保留若干位小数。然后从后往前遍历这个数,如果小数点后是0的话就把他去掉,直到遇到不是0的数,或者除掉小数点后为止。而这个遍历操作我是另外定义了一个方法zero_suppression,实现去零操作。

zero_suppression:
        # 去除所有小数点后以0结尾的数字,如3.50000 执行该方法后变成3.5
        def zero_suppression(nums):
            # 倒序遍历nums
            for i in nums[::-1]:
                if "." in nums and i in "0.":
                    nums = nums[:-1]
                # 直到遇到小数点后的非0数或者没有小数点停止
                else:
                    break
            return nums
formatting_data:
        # 对传进来的表达式的值进行格式化输出
        def formatting_data(exp):
            f = str(format(exp, '.6f'))  # 保留6位小数
            f = zero_suppression(f)
            return f

加减乘除

兄弟们我好想快点发布博客呀,写了好多天了都。

思路:我们看这个输入过程“数字1 运算符1 数字2 运算符2”,当第一次出现运算符时我们用因子1记录数字1,并记录此时运算符1,然后在上显示屏显示因子1和运算符1,下显示屏置于重显示状态。当第二次出现运算符时我们就用因子2记录数字2,并用eval求解“因子1 运算符1 因子2”的值,然后将值给因子1,再记录运算符2。因为没有等号所以不用初始化,重新执行上显示屏显示因子1和运算符,下显示屏置于重显示状态的语句。特殊情况:1. 连续点击运算符时,我们不执行赋值语句,仅记录运算符,并重新显示运算符。2. 0不可作为除数,当其作为除数时,我们要使用警告框给出提示。

        elif button_text in "加+减-乘*除/":
            # 每点击一次加减乘除都会用因子一因子二记录运算符前的数值
            # 如果上一次点击是运算符,这次还是运算符,我们不希望他让因子一加上因子二。因为可能出现点错了,需要更换符号的情况
            if self.last_click not in "加+减-乘*除/等于=":
                # 如果因子1存在则说明是二次点击,此时用因子二记录后一个数值并得出结果
                if self.factor1:
                    # 考虑当0作为除数的情况
                    try:
                        self.factor2 = self.lower_display.get()
                        expression = self.factor1 + self.operator + self.factor2
                        self.factor1 = formatting_data(eval(expression))
                        self.lower_display.set(self.factor1)
                    except ZeroDivisionError:
                        tkinter.messagebox.showwarning("李帅哥温馨提示", "0 不能作为除数!")
                        initialize()
                        # 提前结束,不执行下面语句
                        return
                # 首次点击运算符用因子一记录数值
                else:
                    self.factor1 = self.lower_display.get()
            # 记录运算符
            self.operator = " %s " % button_text[-1]
            # 每点击一次运算符我们上方显示屏都应该有一定的反馈
            self.upper_display.set(self.factor1 + self.operator)

到这里运算符分支就完成了。 

求平方求倒数开根号相反数

因为他们的功能相似,所以我直接把他们放在一起了。在这部分就要考虑更多无效输入的情况了,比如0没有倒数,负数不能开根号等,另外,这里暂时只支持整数开根号,不支持小数。所以出现这些情况都要给用户一些相应的反馈,而我依旧是使用警告框。

思路:求平方相反数等的无非就对两个对象处理,要么因子1,要么因子2。区分因子1和因子2就是要看是否输入了运算符,怎么看呢?看上显示屏,上显示屏显示的是计算过程,里面包含因子1和运算符的信息。现在记录下显示屏的值,我使用num记录。四种操作都是对下显示屏的值进行计算,不会影响其他变量的值如,因子1,因子2,运算符的值都不会影响。

  • 求平方:num * num
  • 求倒数: 1 / num
  • 相反数:-num
  • 开根号:使用 pow函数求(num,1/2)的值。

求解过程要注意提示错误输入的信息,出现输入错误后还要对数据进行初始化。然后将结果交给formatting_data方法处理。

处理完数据后我们就要在上显示屏显示信息了。若我们对因子1操作,则上方直接显示求解过程,

过程描述为: 处理前的值 +  按钮值(求平方/求倒数/开根号/相反数)+ 处理后的值。若对因子2操作,因为此时上方已经显示了因子1 + 运算符了,所以我们要在上显示屏显示的末尾加上求解过程,而不是直接覆盖。

因为四个操作的显示内容格式一致,所以我定义了一个方法update用于更新上显示屏数据,当得到处理后的数据后直接调用该方法即可,期间定义了一个flag变量用于标记报错情况,如果出现报错后我们直接初始化,不执行更新上显示屏的方法。

update:
# 进行求倒数求平方开根号运算时更新上下方显示屏数据
        def update(n, res):

            # 对因子1进行求倒数求平方开根号运算时直接显示运算后的结果
            if not self.operator or self.operator not in self.upper_display.get():
                self.upper_display.set(" ( " + n + " " + button_text + ": )" + res)

            # 剩下就是对因子2进行计算,对其进行求倒数求平方开根号计算时,需保留显示因子1及运算符再显示运算结果
            else:
                self.upper_display.set(
                    self.factor1 + self.operator
                    + " ( " + n + " " + button_text + ": )" + res)
            # 下方显示屏无论对谁计算都只显示结果
            self.lower_display.set(res)

 求平方分支代码:

        elif button_text in "求平方求倒数开根号相反数":
            num = self.lower_display.get()
            flag = True
            if self.last_click == "等于=":
                # 因为我们规定输入等于号后将进入新一轮计算,所以我们记录完结果的值后就对数据初始化
                initialize()
            if button_text == "求平方":
                expression = float(num) * float(num)
                result = formatting_data(expression)
            elif button_text == "求倒数":
                try:
                    expression = 1 / float(num)
                    result = formatting_data(expression)
                except ZeroDivisionError:
                    tkinter.messagebox.showwarning("李帅哥温馨提示", "0 没有倒数!")
                    result = 0
                    initialize()
                    flag = False
            elif button_text == "相反数":
                expression = "-" + num
                result = str(eval(expression))
            # 开根号情况
            else:
                try:
                    if int(num) >= 0:
                        expression = pow(int(num), 1 / 2)
                        result = formatting_data(expression)
                    else:
                        tkinter.messagebox.showwarning("李帅哥温馨提示", "负数不能开根号哦!")
                        initialize()
                        flag = False
                except ValueError:
                    tkinter.messagebox.showwarning("李帅哥温馨提示", "暂时只支持正整数开根号哦!")
                    initialize()
                    flag = False
                # 现在倒回来看一下我也忘了出现这个错误是什么情况,都怪我当初没备注,为了炫酷直接把错误信息放提示框上了,没有用中文表达,我错了...
                except ZeroDivisionError:
                    tkinter.messagebox.showwarning("李帅哥温馨提示", "0 cannot be raised to a negative power")
                    initialize()
                    flag = False
            if flag:
                update(str(num), str(result))

 不容易啊兄弟们,终于快写完了

数字0~9

之前老说重显示重显示,重显示算法怎么实现呢?其实重显示只是说的高级一点,容易理解一点,其实就是进行判断如果上一次点击是运算符,下一次点击是数字的话,就要舍弃之前的数值,重新输入数字。当然除了运算符还有开平方那些,就是因为它涉及的按钮比较广而且思路容易理解,所以我才放在后面

我们直接上代码,代码讲的很详细了。

        elif button_text in "0123456789":
            # 若上次是运算符则应重新输入数字,
            # 对数值进行求平方求倒数开根号后不使用运算符而直接输入数字要进行初始化
            # 使用等号得出结果再重新输入数值时需对其进行初始化
            # 当第一个数字为0时要覆盖0值
            if self.last_click in "加+减-乘*除/等于=" or \
                    self.last_click in "求平方求倒数开根号" or \
                    self.lower_display.get() == "0":
                if self.last_click in "等于=求平方求倒数开根号":
                    initialize()
                self.lower_display.set(button_text)
            # 如果上一次点击也为数字则直接将新值插入到字符串后面
            # 不必考虑相反数何时点击的情况
            elif self.last_click in "0123456789小数点.相反数":
                self.entry.insert("end", button_text)

 数字分支搞定了!还 差 最 后 一 个 分 支!

好激动真的,写了三四天,终于要熬出头了。最后还差最后一个啥分支?

小数点

这次代码比较容易理解,稍微看下注释就行,我就不多解释了

        elif button_text == "小数点.":
            # 仅在没有小数点的情况下才能添加小数点,否则不作响应
            if '.' not in self.lower_display.get():
                #  如果要给等号得出的结果加上小数点的话要进行一次初始化,因为点击完等号后相当于开始了新一轮的计算
                if "=" in self.upper_display.get():
                    num = self.lower_display.get()
                    initialize()
                    self.lower_display.set("%s." % num)
                # 否则直接加上小数点
                else:
                    self.entry.insert("end", button_text[-1])

2.7生成可执行文件

在pycharm 的Terminal终端输入命令

pip install pyinstaller

 安装成功的话会提示successfully installed,因为我已经安装这个库了,所以不会出现这个提示。

 安装完依赖库后,执行如下命令,即可生成可执行文件

pyinstaller -F -w main.py
  • -F:表示打包成.exe可执行文件
  • -w: 解决打包成功后运行exe文件闪退的问题
  • (附)-i: 指定要加载的图标路径
  • main.py: 表示代码所在的.py文件名

因为我没有使用-i 命令所以是使用默认的图标;mian.py指的是文件名,如下。

 执行生成命令后,末尾提示completed successfully的字样说明生成成功了

 我们复制这个目录在文件夹打开,找到dist文件夹,进入,即可看到生成的可执行文件。

3.完整代码

import tkinter
import tkinter.messagebox


class TkWindows:

    def __init__(self):
        # 创建窗口对象
        self.win = tkinter.Tk()
        # 设置窗口宽度,高度以及窗口初始位置 x 为小写字母x 第一个加号后面参数是x轴坐标,第二个参数是y轴坐标
        self.win.geometry("360x460+570+200")
        # 定义拉伸的最大尺寸
        self.win.maxsize(width=360, height=460)
        # 定义窗口名称
        self.win.title("李帅哥的标准型计算器")

        # 初始化数据
        self.lower_display = None  # 上显示屏
        self.upper_display = None  # 下显示屏
        self.entry = None
        self.label = None

        self.operator = ""  # 运算符
        self.factor1 = ""
        self.factor2 = ""
        self.last_click = ""  # 上一次点击

        # 调用start方法
        self.start()
        # 进入消息循环
        self.win = tkinter.mainloop()

    # 构建start方法用于调用生成两个窗体容器, 一个窗体作为显示屏显示数据, 另一个窗体作为按钮用于接收数据
    def start(self):
        tkinter.messagebox.showinfo("李帅哥温馨提示", "千山万水总是情 给个关注行不行\n创作不易 我是李帅哥 咋们下期见")
        self.button_frame()
        self.text_frame()

    # 显示屏——双显
    def text_frame(self):
        text_frame = tkinter.Frame(self.win, width=360, height=190)
        text_frame.pack(side="top")
        # 定义上下层显示屏可变追踪对象用于及时更新数据
        self.lower_display = tkinter.StringVar()
        self.upper_display = tkinter.StringVar()
        # 设定下层显示的初始值
        self.lower_display.set("0")

        # 设定下层显示组件, entry———单行输入显示,可复制,可直接通过键盘更改文本内容
        self.entry = tkinter.Entry(text_frame, font=("loMa", 34, "bold"),
                                   textvariable=self.lower_display, bd=0,
                                   cursor="circle", justify="right", bg="LightGray")
        # 设定上层显示组件, label——可多行显示,不可复制,不可直接通过键盘更改文本内容
        self.label = tkinter.Label(text_frame, font=("loMa", 14),
                                   textvariable=self.upper_display, bd=0,
                                   anchor="se", bg="LightGray")

        # 设定组件的放置位置及大小, entry没有高度这个属性,而默认高度显示不够美观,因为place布局可以自定义组件的高宽度,所以采用place布局
        self.label.place(x=1, y=10, width=359, height=70)
        self.entry.place(x=1, y=50, width=359, height=120)

        # self.label.pack(side="top")
        # self.entry.pack(side="bottom")

    # 按键
    def button_frame(self):
        button_frame = tkinter.Frame(self.win, width=370, height=270)
        button_frame.pack(side="bottom")
        # 设定计算器按键的顺序, 当按键名称长度大于一时用花括号括起来,方便后续判断
        button_str = "%{CE}{清零}{删除}{求倒数}{求平方}{开根号}{除/}789{乘*}456{减-}123{加+}{相反数}0{小数点.}{等于=}"
        ind = 0
        row = 0
        col = 0
        length = len(button_str)
        while ind < length:
            # 当遇到花括号时(名称长度大于一),按键名为花括号里边的元素
            if button_str[ind] == "{":
                # 找到离该左花括号最近的右花括号
                end = button_str.index('}', ind)
                st = button_str[ind + 1:end]
                # 指针移动到右花括号位置
                ind = end
            # 当名称长度为1时
            else:
                st = button_str[ind]
            # 设置按钮属性
            button = tkinter.Button(button_frame, text=st, font=("loMa", 14),
                                    width=8, height=2)
            # 使用grid网格布局放置按钮
            button.grid(row=row, column=col)
            # 按钮绑定事件click_button_event,当鼠标左键单击按钮时触发该事件
            button.bind("<Button-1>", self.click_button_event)

            col += 1
            # 每行放置四个按钮, 每满四个换一行
            if col >= 4:
                row += 1
                col = 0
            ind += 1

    # 定义按钮事件
    def click_button_event(self, event):

        # 获取当前点击的按钮名称
        button_text = event.widget["text"]

        # 去除所有小数点后以0结尾的数字,如3.50000 执行该方法后变成3.5
        def zero_suppression(nums):
            # 倒序遍历nums
            for i in nums[::-1]:
                if "." in nums and i in "0.":
                    nums = nums[:-1]
                # 直到遇到小数点后的非0数或者没有小数点停止
                else:
                    break
            return nums

        # 对传进来的表达式的值进行格式化输出
        def formatting_data(exp):
            f = str(format(exp, '.6f'))  # 保留6位小数
            f = zero_suppression(f)
            return f

        # 对各个数据初始化
        def initialize():
            self.lower_display.set("0")
            self.upper_display.set("")
            self.factor1 = ""
            self.factor2 = ""
            self.operator = ""
            self.last_click = ""

        # 进行求倒数求平方开根号运算时更新上下方显示屏数据
        def update(n, res):

            # 对因子1进行求倒数求平方开根号运算时直接显示运算后的结果
            if not self.operator or self.operator not in self.upper_display.get():
                self.upper_display.set(" ( " + n + " " + button_text + ": )" + res)

            # 剩下就是对因子2进行计算,对其进行求倒数求平方开根号计算时,需保留显示因子1及运算符再显示运算结果
            else:
                self.upper_display.set(
                    self.factor1 + self.operator
                    + " ( " + n + " " + button_text + ": )" + res)
            # 下方显示屏无论对谁计算都只显示结果
            self.lower_display.set(res)

        if button_text in "清零CE%":
            # 当点击按钮为"CE"时,我们下显示屏清零。但如果上一次点击是等于号的话我们进入一次新运算,要进行初始化。
            if button_text == "CE" and self.last_click != "等于=":
                self.lower_display.set("0")
            else:
                initialize()

        elif button_text == "删除":
            # 删除最后一个字符
            if self.last_click in "0123456789小数点.":
                # 分析删除一个字符后的情况
                text = self.lower_display.get()[:-1]
                if len(text) == 0:
                    self.lower_display.set("0")
                else:
                    self.lower_display.set(text)
            # 如果上一次点击是等号,则需保留下方数据,清除上方数据
            elif self.last_click == "等于=":
                self.upper_display.set("")

        elif button_text in "加+减-乘*除/":
            # 每点击一次加减乘除都会用因子一因子二记录运算符前的数值
            # 如果上一次点击是运算符,这次还是运算符,我们不希望他让因子一加上因子二。因为可能出现点错了,需要更换符号的情况
            if self.last_click not in "加+减-乘*除/等于=":
                # 如果因子1存在则说明是二次点击,此时用因子二记录后一个数值并得出结果
                if self.factor1:
                    # 考虑当0作为除数的情况
                    try:
                        self.factor2 = self.lower_display.get()
                        expression = self.factor1 + self.operator + self.factor2
                        self.factor1 = formatting_data(eval(expression))
                        self.lower_display.set(self.factor1)
                    except ZeroDivisionError:
                        tkinter.messagebox.showwarning("李帅哥温馨提示", "0 不能作为除数!")
                        initialize()
                        # 提前结束,不执行下面语句
                        return
                # 首次点击运算符用因子一记录数值
                else:
                    self.factor1 = self.lower_display.get()
            # 记录运算符
            self.operator = " %s " % button_text[-1]
            # 每点击一次运算符我们上方显示屏都应该有一定的反馈
            self.upper_display.set(self.factor1 + self.operator)

        elif button_text in "0123456789":
            # 若上次是运算符则应重新输入数字,
            # 对数值进行求平方求倒数开根号后不使用运算符而直接输入数字要进行初始化
            # 使用等号得出结果再重新输入数值时需对其进行初始化
            # 当第一个数字为0时要覆盖0值
            if self.last_click in "加+减-乘*除/等于=" or \
                    self.last_click in "求平方求倒数开根号" or \
                    self.lower_display.get() == "0":
                if self.last_click in "等于=求平方求倒数开根号":
                    initialize()
                self.lower_display.set(button_text)
            # 如果上一次点击也为数字则直接将新值插入到字符串后面
            # 不必考虑相反数何时点击的情况
            elif self.last_click in "0123456789小数点.相反数":
                self.entry.insert("end", button_text)

        elif button_text == "小数点.":
            # 仅在没有小数点的情况下才能添加小数点,否则不作响应
            if '.' not in self.lower_display.get():
                #  如果要给等号得出的结果加上小数点的话要进行一次初始化,因为点击完等号后相当于开始了新一轮的计算
                if "=" in self.upper_display.get():
                    num = self.lower_display.get()
                    initialize()
                    self.lower_display.set("%s." % num)
                # 否则直接加上小数点
                else:
                    self.entry.insert("end", button_text[-1])
        elif button_text == "等于=":
            # 出现多次点击等号时保留因子二不变,让结果与因子二计算
            if self.last_click != button_text:
                # 仅在首次点击等号时记录因子二
                self.factor2 = self.lower_display.get()
            # 正常情况直接计算结果
            if self.operator:
                expression = self.factor1 + self.operator + self.factor2
            # 非正常情况点击数字后直接点击等号,上层将显示如 "8="
            else:
                expression = self.factor2
            # 用因子一保存结果
            try:
                self.factor1 = formatting_data(eval(expression))
                self.lower_display.set(self.factor1)
                self.upper_display.set(expression + " = ")
            except ZeroDivisionError:
                tkinter.messagebox.showwarning("李帅哥温馨提示", "0 不能作为除数!")
                initialize()

        elif button_text in "求平方求倒数开根号相反数":
            num = self.lower_display.get()
            flag = True
            if self.last_click == "等于=":
                # 因为我们规定输入等于号后将进入新一轮计算,所以我们记录完结果的值后就对数据初始化
                initialize()
            if button_text == "求平方":
                expression = float(num) * float(num)
                result = formatting_data(expression)
            elif button_text == "求倒数":
                try:
                    expression = 1 / float(num)
                    result = formatting_data(expression)
                except ZeroDivisionError:
                    tkinter.messagebox.showwarning("李帅哥温馨提示", "0 没有倒数!")
                    result = 0
                    initialize()
                    flag = False
            elif button_text == "相反数":
                expression = "-" + num
                result = str(eval(expression))
            # 开根号情况
            else:
                try:
                    if int(num) >= 0:
                        expression = pow(int(num), 1 / 2)
                        result = formatting_data(expression)
                    else:
                        tkinter.messagebox.showwarning("李帅哥温馨提示", "负数不能开根号哦!")
                        initialize()
                        flag = False
                except ValueError:
                    tkinter.messagebox.showwarning("李帅哥温馨提示", "暂时只支持正整数开根号哦!")
                    initialize()
                    flag = False
                # 现在倒回来看一下我也忘了出现这个错误是什么情况,都怪我当初没备注,为了炫酷直接把错误信息放提示框上了,没有用中文表达,我错了...
                except ZeroDivisionError:
                    tkinter.messagebox.showwarning("李帅哥温馨提示", "0 cannot be raised to a negative power")
                    initialize()
                    flag = False
            if flag:
                update(str(num), str(result))

        if button_text not in "删除清零CE%":
            self.last_click = button_text
        # 每执行完一次后将光标移动到最后面位置(考虑到有时候会通过键盘输入某些字符)
        self.entry.icursor("end")
        # 当输入的文本长度大于文本框长度时会显示更近输入的内容
        self.entry.xview("end")


if __name__ == '__main__':
    TkWindows()

想说的话

作为一名刚起步的程序员,产品出现漏洞,代码运行效率不高等都是再正常不过的现象,我也是尽了自己最大能力去做好这一件事,希望大家多多包涵。

如遇BUG可在下方留言跟我说明情况,我会尽快修复,完善代码。另外,前面我提到的软件缺陷,希望有感兴趣的大佬能给我一些宝贵的意见改进这款计算器,我也非常期待这个计算器的2.0版本

  • 13
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

愿此后再无WA

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值