带tkinter的超长检查表框

2024-10-05 10:50:19 发布

您现在位置:Python中文网/ 问答频道 /正文

我正在使用来自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选项。我不知道如何避开这个问题


Tags: inself列表forvaluevarrootvars
2条回答

我也有类似的需求,并拼凑了这个控件小部件复选框,它是基于大量的web示例。如果我知道我会分享这段代码,我会保留这些参考资料。 注意:我是拼凑起来的,并没有像ScrolledWindow类那样创建主要部分

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

有关项目的应用程序数据: 任何像tooltext这样的id文本

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()

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

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,而不是在checkbutton上调用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")

相关问题 更多 >

    热门问题