在这之前已经陆续记录了两篇
第一篇主要是大概理了一下思路(已经能实现功能);
第二篇主要是重新整理了代码,修改了一些逻辑;
就内容方法来说,都是换汤不换药,获取“形状”的那部分还很不成熟,但也够用了(主要是懒得优化了)。优化结构之后便想着给它加一个交互界面,这样用起来就很舒坦了,同时还能练练手,继续学习一点GUI。
所以,这篇博客主要是记录一下UI的构建。主要功能的实现之前也已经记录。
文章目录
关于之前版本
版本 | 说明 |
---|---|
1.0[链接] | 详细记录了思路、实现方法。 功能已经初步实现,代码结构稍显混乱。 |
2.0[链接] | 改进了代码结构,以及一些小的代码逻辑, 主要实现方法没有改观(人懒了,能用就..) |
UI设计开始
这个之前已经画了一个饼[链接在这里],UI是前久就肝好的,当时没有记录,不想只是草草的几句话,然后贴代码。
总觉得还是要用点心记录总结啊,要不然总是原地踏步,“书海相遇,交还书海”?!!!哈哈哈哈
之前画的饼:
(来一个倒序…)
UI框架构建所需部件
UI库 | 部件 | 说明 |
---|---|---|
tkinter | 按钮(Button) | 用于路径、颜色、图形、开始等选择的触发 |
文本框(Entry) | 用于路径的填写/展示,以及颜色的直观与数字展示 | |
标尺(Scale) | 用于数值相关参数的选择 | |
文件选择对话框(filedialog) | 用于路径相关的操作 | |
调色板(colorchooser) | 用于颜色的选择 |
所需函数、模块梳理引用
(正向梳理…)
根据之前版本对代码的梳理,要完成贴图,需要用到的参数有:路径类(字符串)、颜色(字符串)、数字参数类(数型)共三类。
- 至于路径,可以由tkinter自带的filedialog很方便进行选择,同时也可以设有文本框进行显示或者手动输入。
- 至于颜色,同样可以用tkinter自带的调色板直接选择,并用文本框辅助显示或者手动显示。
- 至于数值类的参数,有很多选择:标尺、文本框、选值框spinbox。但是由于后两者有人为输入或是调节数值大小并不是那么方便(个人觉得),且容易输入错误卡bug。所以,就一刀切,直接用标尺,直接限制参数类型、范围、刻度。
路径之类的,感觉还是直接用filedialog的好,不容易卡bug。所以可以限制路径显示文本框的状态属性为 readonly(只读),从而避免认为失误改变路径内容;
颜色也同理,毕竟还是用眼选方便,直接输入颜色的数值这可就太淦。
标尺的话,需要根据数值参数的具体作用,进而选择合适的范围、刻度。
-
路径相关:filedialog
- 文件路径的选择(打开、保存文件)
-
颜色选择 :colorchooser
- 贴图所用底图的背景颜色选择
-
各组件
- 文本框Entry、标尺Scale、按钮Button——参数的选择、显示
-
更多交互
- 消息框:messagebox
- 进度条:progressar
from tkinter.ttk import Progressbar # 进度条
from tkinter import Tk, Button, Scale, Entry, HORIZONTAL, StringVar, IntVar
from tkinter.colorchooser import askcolor # 调色板
from tkinter.filedialog import asksaveasfilename, askopenfilename, askdirectory # 文件选择对话框
from tkinter.messagebox import askyesno, showinfo
参数梳理、UI初步搭建
所需参数梳理并作初始化
特殊类别参数 | 具体实现 |
---|---|
字符类 | StringVar |
数字类 | IntVar |
用tkinter自带这两个对象,主要是可以运用其 set 函数与 get 函数来完成数值的置入与获取,同时还可以与一些组件很好的关联更加方便参数的定向获取。
参数的具体意义参考代码注释,以及之前两篇文章
版本 | 说明 |
---|---|
1.0[链接] | 详细记录了思路、实现方法。 功能已经初步实现,代码结构稍显混乱。 |
2.0[链接] | 改进了代码结构,以及一些小的代码逻辑 |
def __init__(self):
Heart.__init__(self)
self.root = Tk()
self.root.title('请选择相关参数【贴图UI】') #窗口标题
self.root.geometry('700x540') # 窗口大小。组件放置过程中不断调整
self.root.attributes("-alpha", 0.97) # 窗口透明度
self.root.configure(background='beige') # 窗口背景颜色
self.imgs_path = StringVar() # 贴图所存的文件夹路径
self.source_path = StringVar() # 黑白图/形状图的路径
self.img_outpath = StringVar() # 目标图片的输出路径
self.quality = IntVar() # 目标文件的输出品质
self.quality.set(40) # 置初始值
self.leap = IntVar() # 跳行数
self.leap.set(30)
self.per_size = IntVar() # 每张小贴图的大小
self.per_size.set(200)
self.dx = IntVar() # 贴图的横向间距
self.dx.set(20)
self.dy = IntVar() # 贴图的纵向间距
self.dy.set(22)
self.bg = StringVar() # 画布背景颜色
self.bg.set('#000000')
self.show = 0 # 贴图完成后是否展示
UI组件的布置
可以结合下图看代码。下图的布局并非一步到位,也是搭建过程中不断调整得到的。
窗口第1行,图集路径部分
# 图源
Entry(self.root, textvariable=self.imgs_path,
state='readonly', width=50, font=3,
bg='black', fg='black',
).grid(row=0, column=0, columnspan=3)
Button(self.root, text='选择图集路径',
font=16, command=self.get_imgspath,
width=14,
fg='black', bg='violet'
).grid(row=0, column=3, columnspan=2, sticky='E')
说明:
1、self.imgs_path变量与组件Entry相关联,从而可以完成显示、置值、取值的操作;
2、self.get_imgspath函数与Button组件关联,可以将其功能设计为:点击后触发文件选择对话框,选择后将值置入变量self.imgs_path。
3、其它参数就字面意思,详细了解可以翻阅其它博客。
4、接下来主要梳理一下布局的知识(hin重要)【grid、place、pack】
梳理之前先写一下涉及的功能函数:
def get_imgspath(self):
self.imgs_path.set(askdirectory(title='选择图集路径',))
if not self.imgs_path:
if askyesno(title='图集路径未选择', message='是否重新选择图集路径?'):
self.get_imgspath()
else:
pass
Tkinter布局之grid、place、pack梳理
grid(**options)
选项 | 含义 |
column | --指定组件插入的列(0表示第一列) --默认值是0 |
columnspan | --指定用多少列(跨列)显示该组件 |
in_ | --将该组件放到该选项指定的组件中 --指定的组件必须是该组件的父组件 |
ipadx | --指定水平方向上的内边距 |
ipady | --指定垂直方向上的内边距 |
padx | --指定水平方向上的外边距 |
pady | --指定垂直方向上的外边距 |
row | --指定组件插入的行(0表示第一行) |
rowspan | --指定用多少行(跨行)显示该组件 |
sticky | --控制组件在grid分配的空间中的位置 --可以使用N,E,S,W以及他们的组合来定位 --使用加号(+)表示拉长填充,例如N+S表示将该组件垂直拉长填充网格,N+S+W+E表示填充整个网格 --不指定该值则居中显示 |
pack(**options)
选项 | 含义 |
anchor | --控制组件在pack分配的空间中的位置 --N, NE, E, SE, S, SW, W, NW或CENTER来定位(EWSN表示东南西北) --默认值是CENTER |
expand | --指定是否填充父组件的额外空间 --默认值是False |
fill | --指定填充pack分配的空间 --默认值是NONE,表示保持子组件的原始尺寸 --还可以使用的值有:X(水平填充),Y(垂直填充)和BOTH(水平和垂直填充) |
in_ | --将该组件放到该选项指定的组件中 --指定的组件必须是该组件的父组件 |
ipadx | --指定水平方向上的内边距 |
ipady | --指定垂直方向上的内边距 |
padx | --指定水平方向上的外边距 |
pady | --指定垂直方向上的外边距 |
side | --指定组件的放置位置 --默认值是TOP --还可以设置的值有:LEFT,BOTTOM,RIGHT |
place(**options)
选项 | 含义 |
anchor | --控制组件在place分配的空间中的位置 --N, NE, E, SE, S, SW, W, NW或CENTER来定位(EWSN表示东南西北) --默认值是NW |
bordermode | --指定边框模式(INSIDE或OUTSIDE) --默认值是INSIDE |
height | --指定该组件的高度(像素) |
in_ | --将该组件放到该选项指定的组件中 --指定的组件必须是该组件的父组件 |
relheight | --指定该组件相对于父组件的高度 --取值范围是0.0~1.0 |
relwidth | --指定该组件相对于父组件的宽度 --取值范围是0.0~1.0 |
relx | --指定该组件相对于父组件的水平位置 --取值范围是0.0~1.0 |
rely | --指定该组件相对于父组件的垂直位置 --取值范围是0.0~1.0 |
width | --指定该组件的宽度(像素) |
x | --指定该组件的水平偏移位置(像素) --如果同时指定了relx选项,优先实现relx选项 |
y | --指定该组件的垂直偏移位置(像素) --如果同时指定了rely选项,优先实现rely选项 |
窗口第2行,形状图路径部分
# 图形图
self.source_et = Entry(self.root, textvariable=self.source_path,
state='readonly', width=37, font=3, fg='grey',
)
self.source_et.grid(row=1, column=1, columnspan=2,)
self.source_path.set('请选择黑白图....')
self.unlockb = Button(self.root, text='Unlock',
width=6, font=10, height=1,
command=self.unlock, state='disable',
fg='purple', bg='#fff5ee'
)
self.unlockb.grid(row=1, column=0, columnspan=1, pady=1, sticky='')
self.default = Button(self.root, text='默认', font=16,
width=6,
command=self.default_list,
fg='black', bg='violet',
)
self.default.grid(row=1, column=3, sticky='E')
self.source_button = Button(self.root, text='选择', width=7, font=16,
command=self.get_source,
fg='black', bg='violet',
)
self.source_button.grid(row=1, column=4, sticky='W')
说明:
关于形状图(黑白图)的选择逻辑,这里在代码中加入了心形图的数据,于是将其作为默认选项(代码的初衷便就是贴图成心);
*点击默认按钮后,文本框、按钮失效即被锁定,点击Unlock按钮可解除锁定;
*需要选择其他形状的话,可以点击选择按钮选择制作好的黑白图。此时函数self.【黑白图的规范详见之前的文章】
- 将相关变量与文本框关联
- 默认按钮关联函数,其功能大概为:讲贴图数据设为默认,将相应的组件的状态设为disabled,同时置必要的标志参数。
- Unlock按钮关联函数,功能为:重新设置相应组件状态为normal
- 选择按钮关联函数,功能大致为:跳出文件选择窗口,获取形状图路径。
涉及的功能函数:
def unlock(self):
self.if_default = 0
self.source_button['state'] = 'normal'
self.default['state'] = 'normal'
self.source_button['bg'] = 'violet'
self.default['bg'] = 'violet'
self.source_et['fg'] = 'grey'
self.source_path.set('请选择黑白图....')
self.unlockb['state'] = 'disable'
self.unlockb['bg'] = '#fff5ee'
def default_list(self):
if askyesno('确认', '是否选择默认形状?'):
self.lst = lst
self.if_default = 1
self.source_button['bg'] = 'snow'
self.default['bg'] = 'snow'
self.source_button['state'] = 'disable'
self.default['state'] = 'disable'
self.unlockb['bg'] = 'tomato'
self.unlockb['state'] = 'normal'
self.source_et['fg'] = 'red'
self.source_path.set('已锁定默认(心形)')
def get_source(self):
self.source_path.set(askopenfilename(title='选择黑白图路径',
filetypes=[('pic', '.jpg .gif .png .jpeg')])
)
if not self.imgs_path:
if askyesno(title='图片路径未选择', message='是否重新选择?'):
self.get_source()
else:
pass
窗口第3行,输出路径部分
# 输出
Entry(self.root, textvariable=self.img_outpath,
state='readonly', width=50, font=3,
).grid(row=2, column=0, columnspan=3)
Button(self.root, text='选择保存路径', font=16,
command=self.get_imoutpath,
width=14,
fg='black', bg='violet',
).grid(row=2, column=3, columnspan=2, sticky='E')
说明:
*按钮关联函数,功能大致为:跳出文件选择窗口,获取输出路径。
*将相关变量与文本框关联
涉及的功能函数:
def get_imoutpath(self):
self.img_outpath.set(asksaveasfilename(title='选择保存路径',
filetypes=[('pic', '.jpg .gif .png .jpeg')],
defaultextension='.png',
initialfile='wula.png')
)
if not self.imgs_path:
if askyesno(title='图片路径未选择', message='是否重新选择?'):
self.get_imoutpath()
else:
pass
窗口第4至6行,数字参数部分
# 品质
Scale(self.root,
from_=2, to=96,
font=4, length=320,
tickinterval=8,
orient=HORIZONTAL,
resolution=1,
fg='white', bg='black',
bd=2,
variable=self.quality,
label='输出图片品质'
).grid(row=4, column=0, columnspan=2, sticky='W', pady=10)
# 跳行
Scale(self.root,
from_=1, to=98,
font=4, length=320,
tickinterval=10,
orient=HORIZONTAL,
resolution=1,
fg='white', bg='black',
variable=self.leap,
label='跳行步长').grid(row=4, column=2, columnspan=3, sticky='W',pady=10)
# 单张尺寸
Scale(self.root,
from_=20, to=800,
font=4, length=640,
tickinterval=50,
orient=HORIZONTAL,
resolution=20,
fg='gold', bg='black',
variable=self.per_size,
label='单张贴图尺寸').grid(row=5, column=0, columnspan=5, pady=10)
# 间隔
Scale(self.root,
from_=0, to=200,
font=4, length=320,
tickinterval=40,
orient=HORIZONTAL,
resolution=1,
fg='lime', bg='black',
variable=self.dy,
label='纵向间距').grid(row=7, column=0, columnspan=2, sticky='W', pady=10)
Scale(self.root,
from_=0, to=200,
font=4, length=320,
tickinterval=40,
orient=HORIZONTAL,
resolution=1,
fg='lime', bg='black',
variable=self.dx,
label='横向间距').grid(row=7, column=2, columnspan=3, sticky='W', pady=10)
说明:
*要点一:根据参数含义调整设置标尺的范围、刻度;
*要点二:将组件与相关变量关联。
窗口第7行,背景颜色选择部分
# 背景颜色
self.show_c = Entry(self.root, bg=self.bg.get(), state='normal',
font=18, width=8)
self.show_c.grid(row=8, column=0, sticky='E')
self.bg_et = Entry(self.root,
textvariable=self.bg,
state='readonly', width=19, font=16,
fg=self.bg.get(),
)
self.bg_et.grid(row=8, column=1, sticky='E', pady=5)
Button(self.root,
text='选择背景颜色', font=16,
command=self.get_bg,
fg='black', bg='gold'
).grid(row=8, column=2, sticky='W', pady=5)# pady 组件间的纵间距
说明:
*第一个小模块是文本框,用于直观显示所选取的颜色;
*第二个小模块也是文本框,用于所选颜色的16进制显示/输入;
*第三个模块便是按钮,与其关联函数功能设计为:调出调色板获取颜色,并改变之前两个小模块的参数,使所选颜色得以显示。
涉及的功能函数:
def get_bg(self):
self.bg.set(askcolor(title='请选择背景颜色')[-1])
self.bg_et['fg'] = self.bg.get()
self.show_c['bg'] = self.bg.get()
if not self.bg:
if askyesno(title='背景颜色未选择', message='是否重新选择?'):
self.get_bg()
else:
pass
窗口第7行,开始退出按钮部分
Button(self.root, text='开始',
width=10, font=50, height=2,
command=self.test,
fg='orangered', bg='springgreen'
).grid(row=8, column=3, columnspan=1, pady=10)
Button(self.root, text='退出',
width=7, font=20, height=2,
command=self.over,
fg='black', bg='red'
).grid(row=8, column=4, columnspan=1, pady=10)
self.root.mainloop()
说明:
*开始按钮关联函数功能大致为:判断参数是否获取完全、是否合法,如果准备就绪,那么就开始制作(参数传入功能代模块),否则视为没准备好并通过消息窗做出相应提示;
*退出按钮功能则设计为destroy便可。
涉及的功能函数:
# 开始按钮对应
def test(self):
if self.imgs_path.get():
if self.source_path.get() and (self.source_path.get() != '请选择黑白图....'):
if self.img_outpath.get():
self.show = askyesno('绘图', '贴图后是否展示?')
self.mkit(leap=self.leap.get(),
imgs_path=self.imgs_path.get(),
source_path=self.source_path.get(),
out={
'path': self.img_outpath.get(),
'quality': self.quality.get()
},
show=self.show,
per_size=self.per_size.get(),
bg=self.bg.get()
)
#showinfo('完成!', ' 已完成')
'''
if askyesno('bye~~?', '是否结束程序?'):
self.over()
'''
else:
showinfo('提示', '未选择输出路径,请选择并重新确认')
self.get_imoutpath()
#self.test()
else:
if askyesno('未选黑白图', '是否选择已有黑白图?【否则用默认数据】'):
self.get_source()
else:
self.default_list()
#self.test()
else:
showinfo('提示', '未选择图集路径,请选择并重新确认')
self.get_imgspath()
#self.test()
def over(self):
self.root.destroy()
进度条部分
进度条的简单使用可以参考下面这片文章
《进度条的简单使用——点击直达》
self.show_progressbar()
self.p_bar['maximum'] = int((lst[-1][0]-top_yblank)/leap-1)
for line in lst:
#根据leap值筛去不贴图的像素条,达到控制贴图像素条数
if not (line[0] - top_yblank) % leap:
# 当前-行数-(从零开始) [每行高度(per_size+dy)]
column = int((line[0] - top_yblank) // leap)
#print(f'\rget__{column+1}/{lst[-1][0]//leap}__!', end='')
self.p_bar['value'] = column
self.root.update()
for x in range(line[1], line[2]+1, per_size+self.dx.get()):
s_img = openi(next(imgs)).resize((per_size, per_size))
canv.paste(s_img, (x, top_yblank+column*int(per_size+self.dy.get())))
else:
showinfo('完成!', '已完成!')
self.lst = []
self.p_bar.destroy()
self.p_button.destroy()
self.root.geometry('700x540')
self.root.update()
#print('\rAll Done!', end='')
pass
def show_progressbar(self):
self.root.geometry('700x580')
self.p_button = Button(self.root,
text='贴图进度', font=19, state='disable', bg='snow'
)
self.p_button.grid(row=9, column=0, columnspan=1)
self.p_bar = Progressbar(self.root,
length=500, orient=HORIZONTAL,
value=0)
self.p_bar.grid(row=9, column=1, columnspan=4, pady=10)
说明:
*进度条的逻辑为:开始贴图是在窗口中加载出进度条,并根据贴图进度跟新进度条填充状态;当贴图结束时,“去掉”进度条,窗口大小还原。
*进度条的使用是在功能函数内部,如果UI与功能代码分开为两类的话,需要注意子父类继承,使参数、函数串联起来。
至此,这个“小项目”也已经完成。
总结一下。这个练手“小项目”中比较详细的整理了tkinter的布局知识,也涉及了进度条、消息窗的简单使用,还有文件对话框filedialog模块、调色板colorchooser模块的使用,以及几个常用组件的进一步学习(参数的学习,参数的设置关联,根据实际需要着重使用某些参数…)。用于tkinter学习练手是很不错的小项目,理论性的只是看再多也不如动手实践几个小项目来的快啊。