【问题标题】:Very long checklist box with tkinter带有 tkinter 的非常长的清单框
【发布时间】:2020-10-12 15:09:27
【问题描述】:

我正在使用来自this answer 的代码来制作一个带有复选框的列表。

import tkinter as tk

root = tk.Tk()

class ChecklistBox(tk.Frame):
    def __init__(self, parent, choices, **kwargs):
        tk.Frame.__init__(self, parent, **kwargs)
        
        self.vars = []
        bg = self.cget("background")
        for choice in choices:
            var = tk.StringVar(value=choice)
            self.vars.append(var)
            cb = tk.Checkbutton(self, var=var, text=choice,
                                onvalue=choice, offvalue="",
                                anchor="w", width=20, background=bg,
                                relief="flat", highlightthickness=0
            )
            cb.pack(side="top", fill="x", anchor="w")
    
    
    def getCheckedItems(self):
        values = []
        for var in self.vars:
            value =  var.get()
            if value:
                values.append(value)
        return values

choices = [str(e) for e in range(100)]
checklist = ChecklistBox(root, choices, bd=1, relief="sunken", background="white")
checklist.pack()

由于选项列表很长,我想在此列表中添加一个滚动条。最好的方法是什么?


我尝试按照示例here,但ChecklistBox 没有yview 方法,也没有yscrollcommand 选项。我不知道如何规避这个问题。

【问题讨论】:

  • 您可以创建一个Scrollable 框架来包含所有这些复选按钮。

标签: python python-3.x tkinter checkbox scrollbar


【解决方案1】:

问题的根源在于框架不可滚动。因此,您必须找到一个支持滚动的小部件,并将其用作向一组小部件添加滚动的基础。

Canvas 小部件通常用于此目的。它通常与内部框架结合使用,这使得使用packgrid 来排列小部件变得很容易。但是,由于您正在创建相同小部件的垂直堆栈,因此直接在画布上绘制复选按钮会更容易。

第一步是在框架中添加一个画布和滚动条:

class ChecklistBox(tk.Frame):
    def __init__(self, parent, choices, **kwargs):
        tk.Frame.__init__(self, parent, **kwargs)

        canvas = tk.Canvas(self, background=self.cget("background"))
        vsb = tk.Scrollbar(self, command=canvas.yview)
        canvas.configure(yscrollcommand=vsb.set)
        vsb.pack(side="right", fill="y")
        canvas.pack(side="left", fill="both", expand=True)
        ...

接下来,我们将调用create_window,而不是在复选按钮上调用pack。我们可以通过获取上一项的 y 坐标来确定下一项的放置位置。我们将使用框架的pady 选项作为间距。

        pady = int(str(self.cget("pady")))
        for choice in choices:
            ...
            bbox = canvas.bbox("all")
            y0 = pady if bbox is None else bbox[3]+pady
            canvas.create_window(0, y0, window=cb, anchor="nw")

最后,您需要确保scrollregion 设置正确:

        canvas.configure(scrollregion=canvas.bbox("all"))

【讨论】:

  • 我很想知道,如果我们直接继承Canvas而不是继承Frame会有什么不同吗?
  • @Saad:不,不会有任何区别,尽管您不应该将滚动条放在画布内。
  • 感谢您的回复。我在想我们可以跳过Frame,但忘记了滚动条如果包含在Canvas 中会占用一些空间,这不是一个好习惯。
  • 第二个代码块也是类定义的一部分吗?
  • 布莱恩,谢谢! -- 滚轮仍然不起作用,但至少滚动条现在可以使用了。
【解决方案2】:

我也有类似的需求,并根据大量网络示例拼凑了这个控件小部件 CheckListBox。如果我知道我会分享这段代码,我会保留参考资料。 请注意,我将它拼凑在一起,并不是像 ScrolledWindow 类这样的主要部分。

主要特点, 检查 [0,1,-1] 0 未选中,1 已选中,-1 未选中图像 选项项目图片

关于项目的应用数据: 任何 id 文本,如 tooltext

CheckListBox Demo Image

import os, sys
import tkinter as tk
from tkinter import ttk

from PIL import Image,ImageTk

class ScrolledWindow(tk.Frame):

    def __init__(self, parent, canv_w = 400, canv_h = 400, *args, **kwargs):
        self.up = False
        super().__init__(parent, *args, **kwargs)

        self.parent = parent

        # creating a scrollbars
        self.xscrlbr = ttk.Scrollbar(self.parent, orient = 'horizontal')
        self.xscrlbr.grid(column = 0, row = 1, sticky = 'ew', columnspan = 2)
        self.yscrlbr = ttk.Scrollbar(self.parent)
        self.yscrlbr.grid(column = 1, row = 0, sticky = 'ns')  # ***** Grid

        # creating a canvas
        self.canv = tk.Canvas(self.parent , border=1 , borderwidth=2)
        # placing a canvas into frame
        self.canv.grid(column = 0, row = 0, sticky = 'nsew') # ***** Grid
        # accociating scrollbar comands to canvas scroling
        self.xscrlbr.config(command = self.canv.xview)
        self.yscrlbr.config(command = self.canv.yview)

        # creating a frame to inserto to canvas
        self.scrollwindow = ttk.Frame(self.parent, width=canv_w, height=canv_h)
        self.scrollwindow.grid(column = 0, row = 0, sticky = 'nsew')  # grid
        self.canv.create_window(0, 0, window = self.scrollwindow, anchor = 'nw')

        self.canv.config(xscrollcommand = self.xscrlbr.set,
                         yscrollcommand = self.yscrlbr.set,
                         scrollregion = (0, 0, 100, 100) # scrollregion = (0, 0, 100, 100)
                         )

        self.yscrlbr.lift(self.scrollwindow)
        self.xscrlbr.lift(self.scrollwindow)

        self.scrollwindow.bind('<Configure>', self._configure_window)
        self.scrollwindow.bind('<Enter>', self._bound_to_mousewheel)
        self.scrollwindow.bind('<Leave>', self._unbound_to_mousewheel)


    def _bound_to_mousewheel(self, event):
        self.canv.bind_all("<MouseWheel>", self._on_mousewheel)

    def _unbound_to_mousewheel(self, event):
        self.canv.unbind_all("<MouseWheel>")

    def _on_mousewheel(self, event):
        self.canv.yview_scroll(int(-1*(event.delta/120)), "units")

    def _configure_window(self, event):
        # update the scrollbars to match the size of the inner frame
        size = (self.scrollwindow.winfo_reqwidth(), self.scrollwindow.winfo_reqheight())
        self.canv.config(scrollregion='0 0 %s %s' % size)

        if self.scrollwindow.winfo_reqwidth() != self.canv.winfo_width():
            # update the canvas's width to fit the inner frame
            self.canv.config(width = self.scrollwindow.winfo_reqwidth())

        if self.up == True and self.scrollwindow.winfo_reqheight() != self.canv.winfo_height():
        #if self.scrollwindow.winfo_reqheight() != self.canv.winfo_height():
            # update the canvas's width to fit the inner frame
            self.canv.config(height = self.scrollwindow.winfo_reqheight())


class CheckListBox(  tk.Frame ):
    def __init__(self, parent, **kw):
        # x = kw.pop( 'x' )  if 'x' in kw else 300
        # y = kw.pop( 'y' ) if 'y' in kw else 300
        #tk.Toplevel.__init__(self, self.parent, **kw)
        #self.geometry(f'{width}x{height}+{x}+{y}')
        if 'master' in kw and parent is None:
            parent = kw.pop('master')
        self.parent = parent

        tk.Frame.__init__(self, parent, **kw )
        self.height = kw.pop( 'height') if 'height' in kw else 250
        self.width  = kw.pop( 'width') if 'width' in kw else 550

        self.win = ScrolledWindow( self  , self.width , self.height)

        self.tframe= self.win.scrollwindow
        #self.tframe.pack_propagate(False)

        pngd =  os.path.dirname(os.path.realpath(__file__)) + "/_common/images/"
        self.checked = checkedImg = tk.PhotoImage( file= pngd+"Checked_18.png" )
        self.unchecked = uncheckedImg = tk.PhotoImage( file= pngd+"CheckedNot_18.png" )

        #canv.create_image(0, 0, image=photoImg)
        self.items=[]
        self.win.canv.config(height = self.height)
        self.win.up=False

    def getvalue( self, values , matchstr , default ):
         it = matchstr.strip().lower()                                   # case insensitive match
         for idx in range( 1, len(values) ):                      # skip the first entry that is the widget id
            try:                                                                      # do not let an execption stop us
             p = values[ idx ].split( '=')
             if p[0].strip().lower() == it:                               # case insensitive match
                 return p[1]
            except: pass;
         return default                                                       # Return the default

    def setvalue( self, values , matchstr , data=None ):
         it = matchstr.strip().lower()                                   # case insensitive match
         for idx in range( 1, len(values ) ):                      # skip the first entry that is the widget id
            try:
             p = values[ idx ].split( '=')
             if p[0].strip().lower() == it:
                 if data == None or data =="":                    # empty data  indicates deletion request
                        return values.remove( values[ idx ] )   # remove the entry in the list
                 values[idx] = matchstr + '=' + str(data)      # Since it exists update the data
                 return
            except: pass;
         values.append( matchstr + '=' + str(data)  )        # New data so append it
         return

    def OnItemDouble( self, ev, idx ) :
         values = self.items[ idx ]                                       # Get the values assigned to the item at [ idx ]
         b = int( self.getvalue( values, 'chk',0 ) )               # Get the chk value set it to 0 if not set
         b = 0 if b == 1 else 1                                            # Toggle the value and then get the image
         self.setvalue( values, 'chk' , b )                              # Save its new state

         img = self.checked if  b == 1  else self.unchecked
         textctl=ev.widget                                                   # textctl=values[0] either will work
         textctl.config(state='normal')                                # Set state to normale so we can overwrite
         textctl.delete("1.0", "1.1")                                      # Delete the line data including image
         textctl.image_create( "1.0", image=img)              # Add the checkmark image first
         textctl.config( state='disabled')                             # Then set the state to readonly again
         #textctl.config( state='disabled', bg='blue')         # Then set the state to readonly again
         print( values )

    def append( self , txt,  **kw ):
             values=[]

             bchk=int( kw.pop( 'chk')) if 'chk' in kw else -1
             appdata = kw.pop( 'appdata') if 'appdata' in kw else []
             pic = kw.pop( 'image') if 'image' in kw else None

             textctl = tk.Text( self.tframe,height=1,width=self.width, **kw)
             textctl.grid( column=0,row= len( self.items),sticky = 'nsew') # +++ nsew???

             values.append( textctl )                                       # The text control is always first
             self.setvalue( values , 'chk' , str(bchk) )
             self.setvalue( values , 'image' , pic )
             self.setvalue( values , 'text' , txt )

             if bchk>=0:
                 img = self.checked if bchk==1 else self.unchecked
                 textctl.image_create( "end", image=img)

             if pic : textctl.image_create( "end", image=pic)
             textctl.insert("end", txt )

             values += appdata
             self.items.append(  values )
             idx = len( self.items ) -1
             textctl.config(state=tk.DISABLED, highlightthickness = 0, borderwidth=1)
             textctl.bind("<Double-Button-1>", lambda ev=None, x=idx:  self.OnItemDouble( ev, x) )
             return idx

    def insert( self, idx, txt  , **kw ):
             values=[]

             if idx < 0 : return -1
             if idx > len( self.items ):  return self.append( txt,**kw)


             bchk=int( kw.pop( 'chk')) if 'chk' in kw else -1
             appdata = kw.pop( 'appdata') if 'appdata' in kw else []
             pic = kw.pop( 'image') if 'image' in kw else None

             textctl = tk.Text( self.tframe,height=1,width=self.width, **kw)

             values.append( textctl )                                       # The text control is always first
             self.setvalue( values , 'chk' , str(bchk) )
             self.setvalue( values , 'image' , pic )
             self.setvalue( values , 'text' , txt )
             self.items.insert(  idx, values )

             for i in range( 0 , len( self.items )):
                         self.items[i][0].grid_forget()
             for i in range( 0, len( self.items )):
                     values = self.items[i]
                     textctl = values[0]
                     textctl.config(state='normal')                             # To change the text and images
                     textctl.grid( column=0,row=i,sticky='nsew')
                     if i==idx:
                         textctl.delete('1.0','end')
                         bchk = int( self.getvalue(  values ,'chk',0))
                         if bchk >= 0:
                             img = self.checked if bchk == 1 else self.unchecked
                             textctl.image_create( "end", image=img)

                         pic = self.getvalue(  values ,'image',"")
                         if pic !="None" :  textctl.image_create( "end", image=pic)

                         txt = self.getvalue( values, 'text' ,"")
                         textctl.insert( "end", txt )

                     textctl.config(state=tk.DISABLED, highlightthickness = 0, borderwidth=1)
                     textctl.bind("<Double-Button-1>", lambda ev=None, x=i:  self.OnItemDouble( ev, x) )
             return len( self.items )

    def curselection( self ):
         sellist = []
         for idx in range( 0, len( self.items ) ):
             values = self.items[ idx ]
             if int( self.getvalue( values, 'chk', '0') ) > 0:
                 sellist.append( idx )
         return sellist

    def get( self, idx, match='text' , default='N/A'):
         sellist = []
         if idx in range( 0, len( self.items )) :
             values = self.items[ idx ]
             return self.getvalue( values, match,default)
         return default

    def delete( self, idx ):
             if idx < 0  or  idx > len( self.items ):  return -1
             ctl=self.items[idx][0]
             ctl.grid_forget()                      # forget it existed
             ctl=None                                   # delete the existing text control
             return len( self.items )



if __name__ == "__main__":

        def getsel( ctl ):
            lst = ctl.curselection( )
            for i in lst:
                print( "Got ", ctl.get( i ) , ctl.get( i , 'altdata') ,ctl.get(i,'chk') )

        # create a root window.
        top = tk.Tk()
        top.geometry("+300+300")
        top.title("Check Listbox Demo")
        topframe = tk.Frame( top)
        topframe.pack()
        label = tk.Label( topframe, width=10,text="Double click to toggle check.")
        label.pack( side='top',fill='x')


        pngd =  os.path.dirname(os.path.realpath(__file__)) + "/_common/images/"
        fileImg = tk.PhotoImage( file= pngd+"list_16.png" )
        folderImg = tk.PhotoImage( file= pngd+"network_folder_18.png" )

        leftframe = tk.Frame( topframe)
        leftframe.pack(side='left')
        # create listbox object
        example = CheckListBox(leftframe,height=60,width=20)
        example.pack(side="left", fill="both", expand=True)
        for i in range( 0,10):
            data = []
            example.setvalue( data , "altData","Some data here."+str( i +1) )
            example.setvalue( data ,"tip", "Item Tool Tip here")
            pic = folderImg if i%2==0 else fileImg
            example.append(  "First text " + str( i +1) , bg='lightgray' , chk=i%2 , image=pic, appdata=data)


        rightframe = tk.Frame( topframe)
        rightframe.pack(side='left')
        example2 = CheckListBox(rightframe ,height=100,width=30 )
        example2.pack(side="left", fill="both", expand=True)
        for i in range( 0,10):
             if i%2==0:
                 example2.append(  "Second text " + str( i + 1 ) , chk=1 , bg='#C0F862')
             else:
                 example2.append(  "Second text " + str( i + 1 ) , chk=1 , bg='#7FF1C0')

        testframe = tk.Frame( top )
        testframe.pack()


        example3 = CheckListBox(testframe,height=80,width=30 )
        example3.pack()
        for i in range( 0,10):
            if i%2==0:
                example3.append(  "Third text " + str( i + 1 ) , image=folderImg )
            else:
                example3.append( "Third text " + str( i +1), chk=1 , image=fileImg)

        #example.insert(  11, "New First text 11" , chk=1 )
        example.delete( 3)
        example.insert(  3, "New Four " , chk=1 )
        #example.insert(  0, "New First text  1" , chk=1 )

        button = tk.Button( text="print selected in first", command=lambda x=example : getsel(x) ).pack()

        top.mainloop()

【讨论】:

    猜你喜欢
    • 2021-09-25
    • 2017-10-31
    • 1970-01-01
    • 2014-12-19
    • 1970-01-01
    • 2017-08-25
    • 2013-04-18
    • 1970-01-01
    • 2013-09-04
    相关资源
    最近更新 更多