使用tkinter编写程序界面
之前我实现了加密程序所有的功能逻辑,但是目前还只能停留在命令行界面执行,显然是不够友好的,我需要编辑一个简单的UI界面。
在UI库的选择上,我使用了Python内置的tkinter。
这里先安利一个网站:鱼C论坛tkinter,小甲鱼这里发布了N多tkinter的中文使用方法,我这个简陋的UI就是借助鱼C论坛和网上搜索完成的。
一、UI界面排版
大概画了一个UI的草图,之后会尽量根据这个图进行编写。
起初我打算从最简单的开始,就是先搞出一系列标签、输入框、按钮,从界面上看起来完整,然后逐个实现数据传递和函数调用,但是个人感觉这样做真的不好,在最后编写功能实现的时候代码很乱。
我想可以先集中精力搞定一个完整的实例,比如把获取密钥文件的逻辑完全写好,那么签名文件的实现就可以复制后改个名字。
二、逻辑功能
密钥文件选择
这一步有两个选择:filedialog和dnd。前者可创建一个文件对话框,用于打开和保存文件;后者可实现文件夹拖拽。
鱼C论坛里有filedialog的介绍,看了觉得蛮简单,就用它了~
功能实现上,我希望用户自己手动输入文件路径,当然也可以浏览文件夹以选择密钥文件,选择后文件路径会自动填入输入框。
UI上这么显示:
当点击浏览文件时,会弹出文件对话框:
选择文件后,输入框可以填入路径:
下面看代码:
#RSA密钥读取
key_file_label = Label(key_frame,text='密钥文件:') #标签,这里只显示字符
key_file_label.grid(row=0,column=2,padx=20,pady=5) #给标签定位
#输入密钥文件路径
key_filename = StringVar() #实例化一个字符串变量
key_filename_entry = Entry(key_frame,textvariable=key_filename,width=20)
key_filename_entry.grid(row=0,column=3)
#选择密钥文件
def openKeyFile():
global key_filename_entry #如果要在函数中复制,就要声明全局变量
fileName = filedialog.askopenfilename()
key_filename_entry.delete(0,END) #清空输入框已有内容
key_filename_entry.insert(0,fileName) #将文件路径填入输入框
Button(key_frame,text="浏览文件",command=openKeyFile).grid(row=0,column=4)
这段代码用了tkinter中的label(标签)、entry(输入框)、button(按钮)三个组件,第一个参数是组件归属。定义了一个函数,在按钮中调用,可把被选择文件的路径填入输入框,为此要在函数中声明输入框是全局变量。
布局使用了grid(),它比pack()更加灵活。同时,同一个父组件中有了grid(),就不能再用pack(),会报错
_tkinter.TclError: cannot use geometry manager pack inside .46809488 which already has slaves managed by grid
这段代码稍加修改,可以用在签名文件选择和源文件选择,不过代码重复率好高,我暂时还想不到办法简化。
模式选择
用户打开软件,首先要在DES、RSA、混合加密、签名验证中进行选择,这个功能最常见的应该是下拉列表了,不过tkinter的Listbox、OptionMenu都太丑,可以用ttk的Combobox。
from tkinter import *
from tkinter import ttk
#模式选择
mode_label = Label(init_frame,text='模式选择:')
mode_label.grid(row=0,column=0,padx=5,pady=5)
mode = StringVar()
mode.set('DES') #设置默认值
mode_choice = ttk.Combobox(init_frame,textvariable=mode,values=['DES','RSA','混合模式','数字签名'])
mode_choice.grid(row=0,column=1,padx=0,pady=5)
没有from tkinter import ttk
会报NameError: name 'ttk' is not defined
在没有细致学习ttk的情况下,要使用from tkinter import ttk
,而不是from tkinter.ttk import *
,后者会使用ttk的组件代替tkinter的组件,可能造成如fg(前景色)、bg(背景色)等组件风格参数不能使用。
用Combobox实现的下拉列表是这样的:
和常见的下拉列表一样一样的~~值得一说的是,最好在参数中加上state='readonly'
,这会限制用户只能选择下拉列表中的选项,从而避免繁琐的有效输入验证。
同样,相似的代码可用于选择加密解密等操作,以及RSA密钥的位数。
输入框激活
在软件面板中,DES密钥、初始值、密钥文件和签名文件并不是无条件活动的,当不需要它们的时候,输入框应该是不可写状态。在tkingter的组件中有3种state(状态):normal(正常)、disabled(禁用)、readonly(只读),注意写小写时是字符串要加引号,'normal’和’disabled’也可写成NORMAL和DISABLED,这时候不用引号。
模式(操作) | 输入框 |
---|---|
DES | DES密钥和初始值 |
RSA | 密钥文件 |
混合 | 密钥文件 |
签名(签名) | 密钥文件 |
签名(验证) | 密钥文件和签名文件 |
这个表格列出了模式与输入框激活状态的对应关系,例如用户选择了RSA模式,则应该只有密钥文件的输入框是可输入状态。
这个功能我首先想到了输入验证,在尝试2小时后放弃,以下两种验证方法均失败:
- 在mode处验证,期望在模式选择后就对相应输入框的state赋值,以改变状态;实测,根本无法更改
- 在输入框处验证,由于验证validate可设置的值全部与输入框有关,因此只有由激活状态改为不可用状态时成功,当输入框不可用时,无法得到鼠标焦点,也就没办法进行验证
这时候看到了事件绑定,算是把我拯救了,关于事件绑定和验证,还请移步全部是中文的鱼C论坛-事件绑定。
事件绑定的一般语法是:
def handler(event):
...
widget.bind(event,handler)
这个代码意为:组件widget发生事件event时,调用方法handler,在这段代码中,handler(event)中的event不用随着事件名称改变。
我只需找到一个合适的event,使得当模式选择完毕后,自动判断需要哪个输入框,将其他输入框关闭,根据模式框特点,选择FocusOut即失去焦点。
事件绑定在模式选择和操作选择后:
mode_choice.bind('<FocusOut>',judgMode)
operation_choice.bind('<FocusOut>',judgOperation)
然后编写judgState方法就可以了。因为签名和验证需要的文件也不同,所以还要对operation做一个事件绑定。
#模式选择的事件绑定
def judgMode(event):
if mode.get() == 'DES':
des_key_entry['state'] = 'normal'
des_IV_entry['state'] = 'normal'
key_filename_entry['state'] = 'disabled'
sig_filename_entry['state'] = 'disabled'
elif mode.get() == 'RSA' or '混合模式':
des_key_entry['state'] = 'disabled'
des_IV_entry['state'] = 'disabled'
key_filename_entry['state'] = 'normal'
sig_filename_entry['state'] = 'disabled'
elif mode.get() == '数字签名':
des_key_entry['state'] = 'disabled'
des_IV_entry['state'] = 'disabled'
#操作选择的事件绑定
def judgOperation(event):
if operation.get() == '签名':
key_filename_entry['state'] = 'normal'
sig_filename_entry['state'] = 'disabled'
elif operation.get() == '验证':
key_filename_entry['state'] = 'normal'
sig_filename_entry['state'] = 'normal'
下图就是选择数字签名验证后的效果啦,密钥文件和签名文件可输入,DES密钥和初始值是禁用的。
进度对话框
进度输出使用Text组件,此组件据说异常强大和灵活。但是!它没有readonly状态,Text中的disabled其实就是其他组件中的readonly。
要在Text组件中输出文本很简单,只需熟练使用text.insert(END,var)
,END是索引,表示从Text文本缓冲区的最后一个字符的下一个位置插入,var是要插入的值,只需不停的给var赋值就可以输出进度了。
先从简单的入手,点击按钮生成RSA密钥文件,并在对话框输出成功消息。
def rsakey():
textVar = base.geneKeys(int(keys_choice.get()))
text.insert(END,textVar)
text.insert(END,'\n')
#密钥生成按钮
gene_key_button = Button(init_frame,text='生成RSA密钥文件',command=rsakey,width=15)
点击按钮后,获取RSA位数并调用geneKeys()方法,密钥文件生成后将返回值赋给textVar,在Text组件的实例text中输出,并添加一个换行符。为了给变量赋值,geneKeys()返回值要稍作修改,改为return '密钥文件已生成'
点击两次生成密钥文件,测试成功:
三、整合
所有的模式和操作判断逻辑做成一个方法doCrypto,在UI中获取各个参数后调用doCrypto;同时为了在对话框中打印出进度提示,将各方法的返回值进行类似"密钥文件生成"的更改
获取参数很简单,拿模式选择举例,只要mode = mode_choice.get()'
,将所有参数获取后全部丢到doCrypto里就成了。
说起来简单做起来难啊。
- 首先,那么多参数获取,每个写一行感觉太丑。尝试用列表获取,然后回传一个可选参数,这样要涉及到的更改太多,许久之后还在报错就放弃了。先解决’有没有’的问题吧,我丑丑的写了7行参数获取,顺利完成
- 前面变量命名时有点随意,一共使用了两种命名,为了便于识别,把所有的命名都更改一致
第一次整合出现了这个错误:
_tkinter.TclError: wrong # args: should be ".49388048 insert index chars ?tagList chars tagList ...?"
出现这个错误的原因是:我在调用insert的时候,要插入的变量值是None。因为我调用的方法没有设置返回值,在doCrypto()末尾简单添加一行return "操作已完成"
就不会再报错了。
现在的代码在不故意捣乱的情况下,可以正常运行,但是问题多多,体验也不好,已全部MARK,一条条搞定。
测试用例:DES加密和解密,密码和初始值都是12345678,操作全部成功。
github仓库已更新。
接下来要学会封装。