引言

在GUI中,表格作为一种数据呈现的方式组件,会运用到数据反馈等功能中,让用户对程序处理的结果有一种较为直观的感受。因此,绘制表格也是TinUI走向成熟一个重要里程。

实际上,我对tkinter(tcl\tk)自身的表格(Treeview)很不满。首先,列宽虽然能够自定义,但不能够自动适配表头内容,默认情况下,表头宽度是一定的。其次,单元格的内容不能够换行,否则无法显示除了第一行以外的其它内容,而且如果第一行的文本超过了列宽,它居然就不显示了,还得用户自己调整表格宽度。以上的这些缺点,对任何人都不太友好,那么TinUI绘制表格的主要目标就是表格尺寸与内容自动契合。

再次剧透 ,TinUI绘制的表格(又)是目前最复杂的组件绘制。


构思

TinUI的表格究竟如何绘制,还是考虑了一段时间的。

外观

想想那个UWP并没有原生表格控件,其中的表格都是由DataGrid等单元格组件绘制出来的。因此,TinUI也将以单元格的应试绘制表格单元。TinUI的表格也不会支持宽度手动改变,目前也没想要明显区分表头和表格内容。

看起来TinUI绘制表格简化了很多工作,但是我们的最终目的——绘制一个自动契合内容的表格,是必须要达成的。

绘制方法

为了契合表格内容,绘制过程中需要用到大量的计算和平面图形想象。

绘制基本流程如下:

从表头组中获取第n表头内容
绘制表头文本
绘制表头单元格 确定第n列列宽
获取第a行内容组 a>1
获取第b列内容
绘制第b列文本内容
绘制单元格 将单元格高度加入a行高度组
获取第a行单元格最大高度 重绘a行单元格
完成绘制

有了逻辑图,接下来就可以开工了。


布局

函数结构

def add_table(self,pos:tuple,outline='#E1E1E1',fg='black',bg='white',data=[['1','2','3'],['a','b','c']],minwidth=100,font=('微软雅黑',12)):#绘制表格
    '''
    pos::位置
    outline::边框颜色
    fg::文本颜色
    bg::单元格颜色
    data::表格数据。格式:((title,...,...),(content,...,...),...)
    minwidth::最小列宽
    font::字体
    '''

绘制表头

根据逻辑图,绘制表头时,先绘制表头文本,再绘制单元格,同时,将列宽记录到宽度组。

为了方便后面获取特定列的列宽,宽度组的设计如下:

line_width={1:width1,2:width2,3:width3,...}

绘制代码如下:

title_num=len(data[0])#获取表头个数
end_x,end_y=pos#起始位置
height=0
line_width={}#获取每列的固定宽度
count=1#列数
for i in data[0]:#绘制表头
    title=self.create_text((end_x,end_y),anchor='nw',text=i,fill=fg,font=font)
    bbox=self.bbox(title)
    #判断最小宽度
    if bbox[2]-bbox[0]<=100:
        width=100
    else:
        width=bbox[2]-bbox[0]
    #设定该列列宽
    line_width[count]=width
    height=bbox[3]-bbox[1]
    self.create_rectangle((end_x,end_y,end_x+width,end_y+height),outline=outline,fill=bg)
    #下一个表头的起始位置
    end_x=end_x+width+2
    count+=1
    self.tkraise(title)

通过注释和解读代码可知,绘制表头主要分为以下几个部分。

  1. 绘制表头文本和单元格
  2. 获取单元格宽度,并加入宽度字典
  3. 计算下一个表头的起始绘制位置
  4. 重新调整个画布对象的层次

其中的第二、三步就涉及到了平面图形想象和计算。

绘制表格内容

格局逻辑图,可以得出绘制表格内容的基本框架:

for line in data[1:]:
    #绘制每一行
    for a in line:
        #绘制单元格
    #获取最大高度,重绘该行

那么根据绘制表头经验,绘制同一行内容的代码如下:

count=1
a_dict={}#高度字典
end_x=pos[0]#其实横坐标
height=0
for a in line:
    width=line_width[count]
    cont=self.create_text((end_x,end_y),anchor='nw',text=a,fill=fg,width=width,font=font)
    bbox=self.bbox(cont)
    height=bbox[3]-bbox[1]#获取高度
    back=self.create_rectangle((end_x,end_y,end_x+width,end_y+height),outline=outline,fill=bg)#单元格
    self.tkraise(cont)
    a_dict[count]=(back,height,(end_x,end_y,end_x+width))#(end_x,end_y,width)为重新绘制确定位置范围
    end_x=end_x+width+2#该行下一个单元格的起始横坐标
    count+=1

获取高度和重绘

在上面的一段代码中,出现了字典a_dict。其结构如下:

a_dict={列数:(单元格画布ID,高度,(x1,y1,x2))}

通过循环其中的每一个高度,获取最大高度,这个很简单,直接给代码:

def get_max_height(widths:dict):
    height=0
    for i in widths.values():
        height=i[1] if i[1]>height else height
    #重新绘制
    for back in widths.keys():
        self.delete(widths[back][0])
        x1,y1,x2=widths[back][2]
        y2=y1+height
        newback=self.create_rectangle((x1,y1,x2,y2),outline=outline,fill=bg)
        self.lower(newback)
    return height
end_y=pos[1]+height+2
for line in data[1:]:
    #...
    for a in line:
        #...
    height=get_max_height(a_dict)
    end_y=end_y+height+2

经过以上的一波操作,一个能够自定义颜色、适配列宽、自动调整单元格高度以显示全部内容的表格组件已经被绘制出来了。

完整代码函数

def add_table(self,pos:tuple,outline='#E1E1E1',fg='black',bg='white',data=[['1','2','3'],['a','b','c']],minwidth=100,font=('微软雅黑',12)):#绘制表格
    def get_max_height(widths:dict):
        height=0
        for i in widths.values():
            height=i[1] if i[1]>height else height
        #重新绘制
        for back in widths.keys():
            self.delete(widths[back][0])
            x1,y1,x2=widths[back][2]
            y2=y1+height
            newback=self.create_rectangle((x1,y1,x2,y2),outline=outline,fill=bg)
            self.lower(newback)
        return height
    title_num=len(data[0])#获取表头个数
    end_x,end_y=pos#起始位置
    height=0
    line_width={}#获取每列的固定宽度
    count=1
    for i in data[0]:
        title=self.create_text((end_x,end_y),anchor='nw',text=i,fill=fg,font=font)
        bbox=self.bbox(title)
        if bbox[2]-bbox[0]<=100:
            width=100
        else:
            width=bbox[2]-bbox[0]
        line_width[count]=width
        height=bbox[3]-bbox[1]
        self.create_rectangle((end_x,end_y,end_x+width,end_y+height),outline=outline,fill=bg)
        end_x=end_x+width+2
        count+=1
        self.tkraise(title)
    end_y=pos[1]+height+2
    for line in data[1:]:
        count=1
        a_dict={}
        end_x=pos[0]
        height=0
        for a in line:
            width=line_width[count]
            cont=self.create_text((end_x,end_y),anchor='nw',text=a,fill=fg,width=width,font=font)
            bbox=self.bbox(cont)
            height=bbox[3]-bbox[1]
            back=self.create_rectangle((end_x,end_y,end_x+width,end_y+height),outline=outline,fill=bg)
            self.tkraise(cont)
            a_dict[count]=(back,height,(end_x,end_y,end_x+width))#(end_x,end_y,width)为重新绘制确定位置范围
            end_x=end_x+width+2
            count+=1
        height=get_max_height(a_dict)
        end_y=end_y+height+2
    return None

效果

测试代码

def test(event):
    a.title('TinUI Test')
    b.add_paragraph((50,150),'这是TinUI按钮触达的事件函数回显,此外,窗口标题也被改变、首行标题缩进减小')
    b.coords(m,100,5)
def test1(word):
    print(word)
def test2(event):
    ok1()
def test3(event):
    ok2()
def test4(event):
    from time import sleep
    for i in range(1,101):
        sleep(0.02)
        progressgoto(i)

if __name__=='__main__':
    a=Tk()
    a.geometry('700x700+5+5')

    b=TinUI(a,bg='white')
    b.pack(fill='both',expand=True)
    m=b.add_title((600,0),'TinUI is a test project for futher tin using')
    m1=b.add_title((0,680),'test TinUI scrolled',size=2,angle=24)
    b.add_paragraph((20,290),'''     TinUI是基于tkinter画布开发的界面UI布局方案,作为tkinter拓展和TinEngine的拓展而存在。目前,TinUI尚处于开发阶段。如果想要使用完整的TinUI,敬请期待。''',
    angle=-18)
    b.add_paragraph((20,100),'下面的段落是测试画布的非平行字体显示效果,也是TinUI的简单介绍')
    b.add_button((250,450),'测试按钮',activefg='white',activebg='red',command=test,anchor='center')
    b.add_checkbutton((80,430),'允许TinUI测试',command=test1)
    b.add_label((10,220),'这是由画布TinUI绘制的Label组件')
    b.add_entry((250,300),350,30,'这里用来输入')
    b.add_separate((20,200),600)
    b.add_radiobutton((50,480),300,'sky is blue, water is blue, too. So, what is your heart',('red','blue','black'),command=test1)
    b.add_link((400,500),'TinGroup知识库','http://tinhome.baklib-free.com/')
    _,ok1=b.add_waitbar1((500,220),bg='lightgreen')
    b.add_button((500,270),'停止等待动画',activefg='cyan',activebg='black',command=test2)
    bu1=b.add_button((700,200),'停止点状滚动条',activefg='white',activebg='black',command=test3)[1]
    bu2=b.add_button((700,250),'nothing button 2')[1]
    bu3=b.add_button((700,300),'nothing button 3')[1]
    b.add_labelframe((bu1,bu2,bu3),'box buttons')
    _,_,ok2=b.add_waitbar2((600,400),fg='blue')
    b.add_combobox((600,550),text='中考成绩预测',content=('730','740','750','760','770','780'))
    b.add_button((600,480),text='测试进度条(无事件版本)',command=test4)
    _,_,_,progressgoto=b.add_progressbar((600,510))
    b.add_table((180,630),data=(('a','space fans over the world','c'),('you\ncan','2','3'),('I','II','have a dream, then try your best to get it!')))
    b.add_paragraph((300,810),text='上面是一个表格')

    a.mainloop()

最终效果

在这里插入图片描述

2021-12-12新样式

突出表头
突出表头。

2022-1-2新样式

在这里插入图片描述
表头自动匹配换行。

2022-7-3新功能

初始化maxwidth可指定单元格最大宽度,文本自动换行。

2024-1-26新样式

在这里插入图片描述

使用圆角边框

(原来上次更新是2022……

下面是标准明亮和暗黑样式:
在这里插入图片描述在这里插入图片描述


补充说明

在TinUI-2.5.0-版本中,除了一些简单和窗口外组件,所有的TinUI组件的返回值的最后一项统一是固定的:uid,也就是整体组件的画布对象名称。可以通过最后一个返回值来获取表格的尺寸。

github项目

TinUI的github项目地址

pip下载

pip install tinui

结语

这次绘制的表格对于鼠标事件的响应可能有点欠缺,但是相比于外观,其完整显示内容的实用性已经完全达到了。

🔆tkinter创新🔆

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐