Legyen a feladat egy olyan egyéni grafikus vezérlőelem készítése, amely a tkinter modul által kínált görgetősávhoz (Scrollbar) hasonló, és amelynél a csúszkát (slider) a vezetőcsatornában (trough) két módon lehet mozgatni:
– a bal vagy jobb oldali nyomógombokkal az aktuálisan érvényes lépésközzel (resolution), vagy
– a bal egérgombbal a vezetőcsatornába történő klikkeléssel. Ekkor a csúszka erre a helyre ugrik.
A lépésköz, ha nem írjuk felül, alapértelmezésben legyen a vezérlőelem példányosításakor megadott vezetőcsatorna-hossz 1%-a. Tehát, ha mondjuk a csatornahossz 300 pixel, akkor a csúszka minimálisan 3 pixelt mozdul el.
A csúszka vezérlésre úgy használható, hogy minden egyes elmozdulásakor a command nevű konfigurációs paraméternek értékül adott hívható objektum legyen meghívva, és argumentumként a csúszkának a vezetőcsatorna hosszában való relatív helyzetének 0..1 intervallumbeli arányszámát kapja meg. Tehát, ha például a csúszka a vezetőcsatorna felénél van, akkor híváskor 0.5 lesz az átadott érték.
Legyen a készítendő egyéni grafikus vezérlőelem osztályának a neve CustomScrollbar. Hasonlóan a tkinter grafikus vezérlőihez (widget) a CustomScrollbar is rendelkezzen néhány beállítható configurációs paraméterrel (options). Ezek a következők:
trough_width: a vezetőcsatorna szélessége pixelben
trough_height: a vezetőcsatorna magassága pixelben
trough_color: a vezetőcsatorna színe
slider_color: a csúszka színe
resolution: az a pixelben mért lépésköz, amellyel a bal vagy jobb gombok megnyomásakor a csúszka elmozdul.
command: hívható objektum, amelynek híváskor a csúszka vezetőcsatornabeli relatív helyzete float számként át lesz adva.
variable: egy DoubleVar vagy StringVar típusú kontrollváltozó, amellyel a csúszka aktuális relatív helyzete kikérhető.
Ahogy más tkinter grafikus elemek esetében, a CustomScrollbar esetében is a felsorolt konfigurációs paraméterek értékét lehessen kikérni a CustomScrollbar példányra meghívott cget() metódussal, valamint az értéküket beállítani a config() vagy configure() metódusokkal. Az említett két nyilvános metóduson felül legyen lehetőség a csúszkát egy adott pozícióba állítani az osztályban definiált move_slider_to() nyilvános metódussal.
A kívánalmaknak megfelelő CustomScrollbar osztály definícióját láthatjuk alább. A felépítést és a működési logika megértését a részletes kommentek segítik.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 |
# modul: custom_scrollbar_widget.py import tkinter as tk from collections import namedtuple Point = namedtuple('Point', 'x y') class CustomScrollbar(tk.Frame): """A görgetősávhoz (Scrollbar) hasonló grafikus vezérlőelem, amelynél a csúszkát (slider) a vezetőcsatornában (trough) két módon lehet mozgatni: - a bal vagy jobb oldali nyomógombokkal az aktuális lépésközzel, vagy - a bal egérgombbal a vezetőcsatornába történő klikkeléssel. Ekkor a csúszka erre a helyre ugrik.\n A csúszka legkisebb elmozdulási lépésköze (resolution) alapértelmezetten a példányosításkor megadott vezetőcsatorna-hossz 1%-a, ami felülírható. Tehát, ha a csatornahossz 300 pixel, akkor a csúszka minimálisan 3 pixelt mozdul el.\n A csúszka minden egyes elmozdulásakor a command paraméternek értékül adott hívható objektum meg lesz hívva, és argumentumként a csúszkának a vezetőcsatorna hosszában való helyzetének arányszámát kapja a 0..1 intervallumból. Tehát, ha a csúszka a vezetőcsatorna felénél van, akkor híváskor 0.5 lesz az átadott érték.\n A beállítható konfigurációs paraméterek: trough_width: a vezetőcsatorna szélessége pixelben trough_height: a vezetőcsatorna magassága pixelben trough_color: a vezetőcsatorna színe slider_color: a csúszka színe resolution: az a pixelben mért lépésköz, amellyel a bal vagy jobb gombok megnyomásakor a csúszka elmozdul. command: hívható objektum, amelynek híváskor a csúszka vezetőcsatornabeli relatív helyzete float számként át lesz adva. variable: egy DoubleVar vagy StringVar típusú kontrollváltozó, amellyel a csúszka aktuális relatív helyzete kikérhető. """ def __init__(self, master, **options): super().__init__(master) # A beállítható konfigurációs paraméterek alapértelmezett értékeinek meghatározása. self.options_defaults = dict(trough_width=300, trough_height=25, trough_color='white', slider_color='black', resolution=3, command=lambda ratio: None, variable=None) # A beállítható konfigurációs paraméterek értékeinek aktualizálása a példányosításkor megadott értékek szerint. self.options = self.options_defaults | options self.options.update(dict(resolution=self.options.get('trough_width') * 0.01)) # A vezetőcsatorna megvalósítása. self._canvas = tk.Canvas(self, width=self.cget('trough_width'), height=self.cget('trough_height'), bg=self.cget('trough_color'), highlightthickness=0) # A csúszka megvalósítása. self._create_slider() # Mivel a beállítható konfigurációs paraméterek neve eltér a csúszkát és a vezetőcsatornát megvalósító grafikus elemek # paraméterneveitől, ezért a konfigurálhatóság megvalósításához a neveket meg kell feleltetni. self._trough_options_alias = {'trough_width': 'width', 'trough_height': 'height', 'trough_color': 'bg'} self._slider_options_alias = {'slider_color': 'fill'} # A bal és jobb oldali nyomógombok létrehozása, és a csúszkamozgató metódusok hozzárendelése. btn_configs = dict(repeatinterval=30, repeatdelay=100, font=('DejaVu Sans Mono', int(9 / 25 * self.cget('trough_height')))) btn_left = tk.Button(self, text=chr(0x25c0), **btn_configs, command=lambda: self._move_slider('left', self.cget('resolution'))) btn_right = tk.Button(self, text=chr(0x25b6), **btn_configs, command=lambda: self._move_slider('right', self.cget('resolution'))) # A grafikus elemek elrendezése. btn_left.pack(side=tk.LEFT, fill=tk.BOTH) self._canvas.pack(side=tk.LEFT) btn_right.pack(side=tk.LEFT, fill=tk.BOTH) # A vezetőcsatornát megvalósító Canvas elemhez hozzárendeljük a bal egérgomblenyomás eseményt és a csúszka egérpozícióhoz # ugrását megvalósító metódust mint eseménykezelőt. self._canvas.bind('<Button 1>', self._jump_slider_to_mouse_cursor) def _create_slider(self): """A csúszka megvalósítása egy a Canvas elemen megjelenő rombusz alakú sokszög rajzelemként, aminek magassága és szélessége igazodik a vezetőcsatorna, azaz a Canvas elem magasságához. """ through_height: int | float = self.cget('trough_height') initial_slider_points = (Point(0, 0), Point(through_height / 4, through_height / 2), Point(0, through_height), Point(-through_height / 4, through_height / 2)) self._canvas.create_polygon(*initial_slider_points, fill=self.options.get('slider_color'), width=0, tags=('slider',)) def _coords_to_points(self, iterable_of_x_and_y) -> list[Point]: """Az x,y koordinták sorozatát Point példányok listájává alakítja.""" itr = iter(iterable_of_x_and_y) return [Point(x, y) for x, y in zip(itr, itr)] def _points_to_coords(self, iterable_of_points) -> list[int | float]: """A Point példányok sorozatát x,y koordinták listájává alakítja.""" coords = [] for point in iterable_of_points: coords.extend(point) return coords def _get_slider_top_point(self) -> Point: """A csúszka felső csúcspontját adja vissza.""" slider_points: list[Point] = self._coords_to_points(self._canvas.coords('slider')) top_point: Point = list(sorted(slider_points, key=lambda point: point.y)).pop(0) return top_point def _expose_slider_position_ratio(self) -> float: """A csúszka vezetősávhosszhoz viszonyított relatív pozíciójával tér vissza, amire a kontrollváltozó értékét is beállítja.""" ratio = self._get_slider_top_point().x / self.cget('trough_width') if tk_var := self.cget('variable'): tk_var.set(ratio) return ratio def _exec_command(self): """A command paraméter értékeként szereplő hívható objektum meghívása átadva a csúszka vezetősávhosszhoz viszonyított relatív pozícióját. """ self.cget('command')(self._expose_slider_position_ratio()) def _move_slider(self, direction, resolution): """A csúszka resolution lépésközzel direction szerinti bal vagy jobb irányba történő mozgatása.""" top_point: Point = self._get_slider_top_point() if direction == tk.LEFT: if top_point.x - resolution >= 0: self._canvas.move('slider', -resolution, 0) else: self._canvas.move('slider', -top_point.x, 0) if direction == tk.RIGHT: if top_point.x + resolution <= int(self._canvas.cget('width')): self._canvas.move('slider', +resolution, 0) else: self._canvas.move('slider', int(self._canvas.cget('width')) - top_point.x, 0) self._exec_command() def _jump_slider_to_mouse_cursor(self, event): """A csúszka áthelyezése a vezetősávon belül az egérmutató szerinti pozícióba.""" self.move_slider_to(event.x) def move_slider_to(self, x): """A csúszka áthelyezése a vezetősávon belül az x koordináta szerinti pozícióba.""" # A csúszka szélességének kiszámítása. x_coords = self._canvas.coords('slider')[0:7:2] slider_length = max(x_coords) - min(x_coords) # A csúszka mozgatása a megfelelő pozícióba. self._canvas.moveto('slider', int(x - slider_length / 2), 0) # A csúszkapozíció változása miatt a command szerinti hívható objektum meghívása. self._exec_command() def cget(self, option_name: str = ''): """Az option_name konfigurációs paraméter értékének lekérdezése. Argumentum nélkül meghívva egy szótárt ad vissza, amely az összes paraméter-érték párt tartalmazza. """ if not option_name: return self.options try: return self.options[option_name] except KeyError: raise ValueError(f'Nincs ilyen lekérdezhető konfigurációs paraméter: {option_name}') def config(self, **kw): """Az érvényes konfigurációs paraméterek értékét változtatja meg.""" # Csak azokkal a konfigurációs paraméterekkel foglalkozunk, amelyek érvényes névvel lettek megadva. valid_options = {k: v for k, v in kw.items() if (k in self.options)} for option_name, option_value in valid_options.items(): # Csak akkor foglalkozunk az adott konfigurációs paraméterrel, ha annak értékében változás történt. if option_value != self.cget(option_name): # Ha a kontrollváltozó megváltozott, ennek értékét be kell állítani. if option_name == 'variable': self._expose_slider_position_ratio() # A csúszka konfigurációját érintő változás. if option_name in self._slider_options_alias: self._canvas.itemconfigure('slider', {self._slider_options_alias[option_name]: option_value}) # A vezetőcsatorna konfigurációját érintő változás. if option_name in self._trough_options_alias: self._canvas.config({self._trough_options_alias[option_name]: option_value}) # A csúszka pozícióját igazítani kell a vezetőcsatorna új szélességéhez. if option_name == 'trough_width': old_through_width: int | float = self.cget('trough_width') dx = (option_value / old_through_width - 1) * self._get_slider_top_point().x self._canvas.move('slider', dx, 0) if option_name == 'trough_height': # A csúszka magasságát igazítani kell a vezetőcsatorna új magasságához. through_height: int | float = self.cget('trough_height') r = option_value/through_height p0 = self._get_slider_top_point() self._canvas.scale('slider', *p0, r, r) x_coords = self._canvas.coords('slider')[0:7:2] self._slider_length = max(x_coords) - min(x_coords) # A nyomógombok feliratának karakterméretét igazítani kell a vezetőcsatorna új magasságához. new_fontsize = int(9 / 25 * option_value) for widget in self.slaves(): if type(widget) is tk.Button: btn: tk.Button = widget current_font = widget.cget('font').split() current_font_family = current_font[0].strip('{}') new_font = (current_font_family, new_fontsize) btn.config(font=new_font) # A változások aktualizálása a konfigurációs nyilvántartásban. self.options.update(valid_options) configure = config |
Azt, hogy az elkészült CustomScrollbar teljesíti-e az előírt követelményeket egy külön modulban megírt tesztalkalmazással ellenőrizzük. Ez az egyéni görgetősáv működésének és konfigurálhatóságának tesztelését teszi lehetővé olyan módon, hogy az alkalmazás indításakor kirajzol egy kört, valamint a kör kiinduló méretét befoglaló és a kétszer akkora sugarú kört befoglaló négyzeteket. Ezek alatt jelenik meg az egyéni görgetősáv. Ez alatt pedig egy olyan vezérlőpanel, ahol a görgetősáv konfigurálható paramétereinek értékét lehet állítani. Az új értékeket a megfelelő beviteli mezőkbe kell beírni és a „Konfiguráció aktualizálás” gombra kattintva érvényesíteni. Ekkor a görgetősáv az új konfiguráció szerint fog megjelenni. A lépésköz változtatásának hatását természetesen csak a csúszka jobb vagy bal oldali gombokkal történő mozgatásakor érzékeljük. Ha a csúszka elmozdul, akkor a kör mérete csökken vagy nő. Növelni legfeljebb a kiinduló méret kétszeresére lehet.
A tesztalkalmazás custom_scrollbar_test_app modulját a benne definiált osztályokkal láthatjuk alább. A működéshez e modulfájlt kell szkriptként futtatni. A futtatáshoz Python 3.10+ szükséges.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
# modul: custom_scrollbar_test_app.py from __future__ import annotations import tkinter as tk from custom_scrollbar_widget import CustomScrollbar, Point class CirclePanel(tk.Canvas): """Egy kört, valamint a kör kiinduló méretét befoglaló és a kétszer akkora sugarú kört befoglaló négyzeteket rajzolja ki.""" def __init__(self, master: CustomScrollBarTestApp): super().__init__(master, bg='white', width=550, height=220) # Kör kirajzolása a vásznon. r = 50 # A kör sugara. cp = Point(300, 110) # A kör középpontja self._circle_id = self.create_oval((cp.x - r, cp.y - r), (cp.x + r, cp.y + r), fill='lightblue') # A kör kiinduló méretét befoglaló, valamint a kétszer akkora sugarúk kört befoglaló négyzetek kirajzolása. self.create_rectangle(self.bbox(self._circle_id), dash='.') self.create_rectangle((cp.x - 2 * r, cp.y - 2 * r), (cp.x + 2 * r, cp.y + 2 * r), dash='-.') self.center_point = cp self.radius = r def change_size(self, ratio): """A kör méretetét a ratio kétszeresére változtatja. Vagyis, ha a ratio 0..1 közötti szám, akkor a kör az eredeti mérethez képest a kétszeresére nőhet. """ cp, r, q = self.center_point, self.radius, ratio self.coords(self._circle_id, cp.x - r * 2 * q, cp.y - r * 2 * q, cp.x + r * 2 * q, cp.y + r * 2 * q) class SliderPositionViewPanel(tk.Frame): """A csúszka aktuális pozíciójáról tájékoztató komponens. A csúszkapozíció egy címkén jelenik meg számszerűen százalékban.""" def __init__(self, master: CustomScrollBarTestApp): super().__init__(master) # Címkék létrehozása. lbl_position = tk.Label(self, text='A csúszka pozíciója a vezetőcsatorna hosszához képest:', font=('Noto', 12)) lbl_slider_position_value = tk.Label(self, font=('Noto', 12)) # A csúszkapozíció aktuális értékét tároló kontrollváltozó létrehozása. self.slider_position_var = tk.DoubleVar(self, name='slider_position') def format_slider_position_value(var_name, s, operation): """A címkén megjelenő számértéket formázza.""" lbl_slider_position_value['text'] = '{:.1%}'.format(self.getvar(var_name)) # A kontrollváltozó minden értékváltozásakor a számformátum meghatározó függvény meg lesz hívva. self.slider_position_var.trace_add('write', format_slider_position_value) self.slider_position_var.set(0) # a kezdőérték beállítása. # A címkék lehelyezése. lbl_position.grid(row=0, column=0, sticky='w') lbl_slider_position_value.grid(row=0, column=1, sticky='w', padx=0, pady=10) class ControlPanel(tk.LabelFrame): """Az egyéni görgetősáv paramétereinek változtatását teszi lehetővé.""" def __init__(self, master: CustomScrollBarTestApp): super().__init__(master, text='Konfigurálás', font=('Source Sans Pro Semibold', 14)) # A grafikus elemek és kontrollváltozóik létrehozása. common_options = dict(font=('Noto', 12)) lbl_trough = tk.Label(self, text='Vezetőcsatorna', **common_options) lbl_trough_width = tk.Label(self, text='szélesség:', **common_options) trough_width_var = tk.IntVar(self, value=master.custom_scrollbar.cget('trough_width')) ent_trough_width = tk.Entry(self, width=10, textvariable=trough_width_var, **common_options) lbl_trough_height = tk.Label(self, text='magasság:', **common_options) trough_height_var = tk.IntVar(self, value=master.custom_scrollbar.cget('trough_height')) ent_trough_height = tk.Entry(self, width=10, textvariable=trough_height_var, **common_options) lbl_trough_color = tk.Label(self, text='szín:', **common_options) trough_color_var = tk.StringVar(self, value=master.custom_scrollbar.cget('trough_color')) ent_trough_color = tk.Entry(self, width=10, textvariable=trough_color_var, **common_options) lbl_slider = tk.Label(self, text='Csúszka', **common_options) lbl_slider_color = tk.Label(self, text='szín: ', **common_options) slider_color_var = tk.StringVar(self, value=master.custom_scrollbar.cget('slider_color')) ent_slider_color = tk.Entry(self, width=10, textvariable=slider_color_var, **common_options) lbl_resolution = tk.Label(self, text='Lépésköz:', **common_options) resolution_var = tk.IntVar(self, value=int(master.custom_scrollbar.cget('resolution'))) ent_resolution = tk.Entry(self, width=10, textvariable=resolution_var, **common_options) btn_config = tk.Button(self, text='Konfiguráció aktualizálás', **common_options, command=lambda: master.custom_scrollbar.config(trough_width=trough_width_var.get(), trough_height=trough_height_var.get(), trough_color=trough_color_var.get(), slider_color=slider_color_var.get(), resolution=resolution_var.get())) # A grafikus elemek lehelyezése rácsos elrendezésben. lbl_trough.grid(row=0, column=0, sticky='w') lbl_trough_width.grid(row=1, column=0, sticky='e') ent_trough_width.grid(row=1, column=1, sticky='w', padx=10, pady=2) lbl_trough_height.grid(row=2, column=0, sticky='e') ent_trough_height.grid(row=2, column=1, sticky='w', padx=10, pady=2) lbl_trough_color.grid(row=3, column=0, sticky='e') ent_trough_color.grid(row=3, column=1, sticky='w', padx=10, pady=2) lbl_slider.grid(row=4, column=0, sticky='w') lbl_slider_color.grid(row=5, column=0, sticky='e') ent_slider_color.grid(row=5, column=1, sticky='w', padx=10, pady=2) lbl_resolution.grid(row=6, column=0, sticky='w') ent_resolution.grid(row=6, column=1, sticky='w', padx=10, pady=2) btn_config.grid(row=7, column=0, sticky='we', columnspan=2, padx=10, pady=10) class CustomScrollBarTestApp(tk.Tk): """Az egyéni görgetősáv működésének és konfigurálhatóságának tesztelését teszi lehetővé.""" def __init__(self): super().__init__() self.title('Egyéni görgetősáv tesztalkalmazás') # A felületet alkotó komponenspéldányok létrehozása. circle = CirclePanel(self) self.custom_scrollbar = CustomScrollbar(self, trough_width=500) self.custom_scrollbar.config(slider_color='blue', trough_color='light yellow', trough_height=30) self.custom_scrollbar.config(command=lambda q: circle.change_size(q)) slider_position_view_panel = SliderPositionViewPanel(self) self.custom_scrollbar.config(variable=slider_position_view_panel.slider_position_var) control_panel = ControlPanel(self) # A komponenspéldányok lehelyezése rácsos elrendezésben. circle.grid(row=0, column=0, sticky='news', padx=10, pady=10) self.custom_scrollbar.grid(row=1, column=0, sticky='news', padx=10, pady=10) slider_position_view_panel.grid(row=2, column=0, sticky='news', padx=10, pady=10) control_panel.grid(row=3, column=0, sticky='w', padx=10, pady=10) # A csúszka beállítása a vezetősáv közepére. self.custom_scrollbar.move_slider_to(self.custom_scrollbar.cget('trough_width')/2) def run(self): self.mainloop() CustomScrollBarTestApp().run() |
A két modulfájl elérhető a https://github.com/pythontudasepites/custom_scrollbar linken.
A következő képernyőképek különböző konfigurációs beállítások és csúszkaállások mellett mutatják az egyéni görgetősáv kinézetét, valamint a tesztkör méretét.


A bemutatott egyéni kialakítású görgetősáv és a tesztprogram elkészítéséhez szükséges ismeretek a Python tudásépítés lépésről lépésre című e-könyvben megtalálhatók elsősorban a „Grafikus felhasználói felület készítése”, című fejezetben, ami nem véletlenül az utolsó fejezet, mert az azt megelőző többi fejezet tartalmának ismeretét feltételezi, arra épít.
A fenti osztálydefiníciókban szereplő kódokat nem csak azért érdemes tanulmányozni, mert a Python tudásépítés lépésről lépésre című e-könyvben közölt példákhoz képest egy újabb összetett alkalmazási példa, hanem mert a tkinter modulhoz kapcsolódó sok nyelvi eszközt kontextusban használva lehet látni, vagyis megoldási mintákként is szolgálnak (természetesen csak akkor, ha az alapismeretek megvannak). Mindez azért is lehet hasznos, mert a tkinter modul dokumentációja a nyelvi eszköztár tekintetében nem megy részletekbe, egy-egy felmerülő használati kérdést illetően önmagában legtöbb esetben nem ad kellő útmutatást.