Python 3.9 - Tkinter - Custom widget class event bind - user-interface

I'm attempting to create a custom search box class named "TkinterEntryBox" based on Tkinter "Entry" widget that would clear its content when left mouse button is pressed inside text entry area. Currently I'm trying to bind a function called "clear" that is a part of custom class that would clear an input in a parent window that would contain instance of "TkinterEntryBox" widget.
I read that inheriting from "Entry" class would be a preferred way of resolving my problem, but I would like to use composition instead of inheritance, since I don't want to have "Entry" class "leaking" outside my custom class.
Problem is, that while callback function is called as expected its "event" argument contains bound "Entry" class widget instance and not "TkinterEntryBox" instance. It causes and exception, since "Entry" class does not have an "clear" method.
Is it possible to force Tkinter to bind my custom class instead of "Entry" class, so that "event" argument in callback function would contain my custom class instance under "widget" property, so that I can safely call "clear" method? Moreover, since I'm new to Tkinter and GUI programming could someone, please tell me if such an approach of creation of widgets is a valid one? If not, then I would greatly appreciate some pointers how to improve my code.
Here is a rough idea of what I created so far:
Below is the custom entry class which "clear" method I would like to call through event callback:
class TkinterEntryBox:
def __init__(self, parent_window: BaseWindow, events_to_callbacks_bindings: Dict[str, Callable]):
self._tkinter_entry = Entry(parent_window)
self._bind_callbacks_to_events(events_to_callbacks_bindings)
def clear(self) -> None:
self._tkinter_entry.delete(ENTRY_BOX_POINTER_START_INDEX, ENTRY_BOX_POINTER_TO_END)
def input(self) -> str:
return self._tkinter_entry.get()
def set(self, text: str) -> None:
self._tkinter_entry.insert(ENTRY_BOX_POINTER_START_INDEX, text)
def place(self, placement_orientation: Geometry) -> None:
self._tkinter_entry.pack(side=str(placement_orientation))
def _bind_callbacks_to_events(self, events_to_callbacks_bindings: Dict[str, Callable]) -> None:
for event_name, callback_function in events_to_callbacks_bindings.items():
self._bind_callback_to_event(event_name, callback_function)
def _bind_callback_to_event(self, event_name: str, callback_function: Callable) -> None:
self._tkinter_entry.bind(event_name, callback_function)
Here is how I initialize my custom entry box class:
def _initialize_search_entry_box(self, factory: TkinterWidgetFactory) -> TkinterEntryBox:
events_to_callbacks_bindings = {
EVENT_ON_LEFT_MOUSE_BUTTON_DOWN: self._on_left_mouse_button_down
}
search_entry_box = factory.assemble_entry_box(self, INGREDIENT_SEARCH_BOX_PLACEHOLDER, events_to_callbacks_bindings)
return search_entry_box
And here is an callback function that is called when left mouse button is pressed inside entry box input area:
#staticmethod
def _on_left_mouse_button_down(event) -> None:
event.widget.clear()
The error message I'm getting in above function call is:
AttributeError: 'Entry' object has no attribute 'clear'

You can simply add self._tkinter_entry.clear = self.clear after creating the entry:
class TkinterEntryBox:
def __init__(self, parent_window: BaseWindow, events_to_callbacks_bindings):
self._tkinter_entry = Entry(parent_window)
self._tkinter_entry.clear = self.clear
...

Related

Updating a kivy label on a screen when dynamically generating it through a list?

Firstly, disclaimer: I am terribly new to programming and am trying to build my understanding with this project. Additionally, let me say I have searched the forums and found similar posts to mine, but none have the issue of updating a label that has been dynamically generated through a list.
My question is in my code, commented out, but to summarize: I generate buttons and labels for each item in a list. Then the buttons should add and subtract from the linked value in a dictionary. Currently the code does this, but the labels on screen don't update to reflect the new values. Can someone please assist with updating the value for "ordlayout.add_widget(ordlayout.lbl[str(i)])" when calling to updateup and updatedown?
import kivy
kivy.require('1.10.0')
from kivy.app import App
from kivy.uix.label import Label
from kivy.uix.button import Button
from kivy.uix.widget import Widget
from kivy.properties import ObjectProperty
from kivy.uix.behaviors import ButtonBehavior
from kivy.uix.gridlayout import GridLayout
from kivy.uix.scrollview import ScrollView
from kivy.uix.textinput import TextInput
from kivy.lang import Builder
from kivy.uix.dropdown import DropDown
from kivy.base import runTouchApp
from kivy.uix.screenmanager import ScreenManager, Screen, FadeTransition
from functools import partial
#The first screen the app opens to. Contains all other screen branches.
class MainScreen(Screen):
pass
#NewOrder will be the screen used for choosing which
#items/and how many of each the customer wants added.
class NewOrder(Screen):
def __init__(self, **kwargs):
super(NewOrder, self).__init__(**kwargs)
#This will eventually read/create a list of strings from a user-modified file.
self.foods = ["Puppy", "Cat", "Fish"]
#I create a dictionary linking a number to each item.
self.countfoods = {}
for i in self.foods:
self.countfoods[i] = 0
#Now I create a grid layout to put on the screen.
ordlayout = GridLayout()
ordlayout.cols = 8
ordlayout.row_default_height=20
ordlayout.buttons={}
ordlayout.btns1 = {}
ordlayout.lbl = {}
#The items I want on the screen are 1.)First item from list. 2.) Minus button.
#3.) Current number of the item. 4.) Plus button.
#I want these four buttons for each item.
for i in self.countfoods:
#Adds text for first item.
ordlayout.add_widget(Label(text=i))
#Adds a button for minus, linked to a unique dict value.
ordlayout.buttons[str(i)] = Button(text="-")
ordlayout.lbl[str(i)] = Label(text=str((self.countfoods[i])))
#The below assigns the specific object location of each label
#to a variable for passing to ocuntup and countdown.
tempPlacement = str(ordlayout.lbl[str(i)])
ordlayout.buttons[str(i)].bind(on_press=partial(self.updatedown, i))
ordlayout.add_widget(ordlayout.buttons[str(i)])
#Add the value that I want to update.
ordlayout.add_widget(ordlayout.lbl[str(i)])
#Adds a button for addition, but doesn't properly link it to a specific value.
ordlayout.btns1[str(i)] = Button(text="+")
ordlayout.btns1[str(i)].bind(on_press=partial(self.updateup, i))
ordlayout.add_widget(ordlayout.btns1[str(i)])
#Add that grid wit
h values to the screen.
self.add_widget(ordlayout)
#Function used to change value down by one.
def updatedown(self, event, i):
self.countfoods[event] -= 1
print (self.countfoods)
#Function used to change value up by one.
def updateup(self, event, i):
self.countfoods[event] += 1
print (self.countfoods)
#AdminOpt will be the screen used for
class AdminOpt(Screen):
def __init__(self, **kwargs):
super(AdminOpt, self).__init__(**kwargs)
#Will allow for opening and checking of created orders.
class OrdHist(Screen):
pass
#This is purely the class used for managing the other screens.
class ScreenManagement(ScreenManager):
pass
Main = Builder.load_file("Order Handler2.kv")
class Customer:
def __init__(self, name, pricelist):
self.name = name
self.pricelist = pricelist
class SimpleKivy(App):
def build(self):
return Main
if __name__== "__main__":
SimpleKivy().run()
Haven't been able to test this (your question is missing your kv file), but something like this might work:
#Function used to change value down by one.
def updatedown(self, i, button):
self.countfoods[i] -= 1
self.ordlayout.lbl[str(i)].text = str(self.countfoods[i])
print (self.countfoods)
#Function used to change value up by one.
def updateup(self, i, button):
self.countfoods[i] += 1
self.ordlayout.lbl[str(i)].text = str(self.countfoods[i])
print (self.countfoods)
You will also need to replace every occurence of ordlayout with self.ordlayout in the __init__() method.
As an aside, you don't need to do str(i) for your dictionary keys. In fact, you can use lists instead of dictionaries, if you prefer.

How to clear more than one tkinter entry field using a function

I have a rather simple problem which is when I am in a particular tkinter entry field on my GUI when I press the backspace key it deletes the field of any text.
All was going well until I wanted to use this method on more than one entry field.
The code
import tkinter
class BuildWidgets:
def __init__(self, master):
self.master = master # reference the main window
self.BACKSPACE = ''
self.TextBox = tkinter.Entry(self.master,width=10)
self.TextBox.bind("<Key>", self.clearTextBox)
self.TextBox.pack()
self.TextBox2 = tkinter.Entry(self.master,width=10)
self.TextBox2.bind("<Key>", self.clearTextBox2)
self.TextBox2.pack()
def clearTextBox(self,event):
pressed = event.char
if pressed == self.BACKSPACE:
self.TextBox.delete(0,'end')
def clearTextBox2(self,event): #I want to eliminate repetitive methods
pressed = event.char
if pressed == self.BACKSPACE:
self.TextBox2.delete(0,'end')
I basically bind the key event to the entry field and the callback checks if backspace was pressed and if so deletes the text. However, when I add a second entry fields I need to create a second method and it goes on and on for each entry field! This is because I cannot find a way to pass the entry to field to a single method as it is a callback in a widget.
Question
How do I, or can I , create a single clear method that distinguishes which entry field is calling it?
The event object that is passed to the function has a widget attribute that is the widget that caught the error.
def clearTextBox(self,event):
pressed = event.char
if pressed == self.BACKSPACE:
event.widget.delete(0,'end')

Works with QGridLayout not with QVBoxLayout

from PyQt4.QtCore import *
from PyQt4.QtGui import *
import sys, os, time
class SetName(QWidget):
def __init__(self):
QWidget.__init__(self)
self.show()
toplayout = QVBoxLayout()
self.setWindowTitle('Personal Info')
self.form_layout = QFormLayout()
self.setLayout(self.form_layout)
self.line_edit_param = QLineEdit(self)
self.line_edit_param.setPlaceholderText("Write Here")
self.form_layout.addRow('Write Name', self.line_edit_param)
toplayout.addLayout(self.form_layout)
self.setFocus()
class LearnApp(QDialog):
def __init__(self):
super(QDialog, self).__init__()
self.setWindowTitle("LearnApp")
self.active = False
close_button = QPushButton("Close")
close_button.clicked.connect(self.close)
self.check_button = QPushButton("Check")
self.check_button.clicked.connect(self.set_data)
self.tr = QTextEdit()
self.tr.setReadOnly(True)
# layout
layout = QHBoxLayout()
#layout.addWidget(self.button3)
sub_layout = QVBoxLayout()
sub_layout.addWidget(self.check_button)
sub_layout.addWidget(close_button)
layout.addLayout(sub_layout)
layout.addWidget(self.tr)
self.setLayout(layout)
self.setFocus()
def set_data(self):
print "in set_data"
SetName()
app = QApplication(sys.argv)
dialog = LearnApp()
dialog.show()
app.exec_()
This is the code I'm trying. If edit it with toplayout = QGridLayout(), program works fine but with toplayout = QVBoxLayout(), it gives message QLayout::addChildLayout: layout "" already has a parentand just flashes the new window. What could be the problem? How should I tackle this? I wanna use QVBoxLayout instead of QGridLayout
Firstly, the new window disappears straight away because you don't store a reference to it. You need to store a reference to the instance in your LearnApp class, or parent it to another Qt object outside of set_data() if you want it to stick around.
The error message regarding the layouts is not occurring because of your choice of layouts, but because you are calling
self.setLayout(self.form_layout)
and then
toplayout.addLayout(self.form_layout)
The first call assigns the layout to the instance of SetName, but in doing so also makes the instance the parent of self.form_layout. The second call is trying to add the same layout to toplayout and set it as the parent, but Qt sees that self.form_layout already has a parent (i.e. is being used elsewhere). This is what the error message is trying to tell you.
I suspect that instead of self.setLayout(self.form_layout), you intended to write something like
self.setLayout(toplayout)

wxPython ListCtrl OnClick Event

So, I have a wxPython ListCtrl which contains rows of data. How can I make an event that calls a function, with the row contents, when one of the rows if clicked on?
You can use the Bind function to bind a method to an event. For example,
import wx
class MainWidget(wx.Frame):
def __init__(self, parent, title):
super(MainWidget, self).__init__(parent, title=title)
self.list = wx.ListCtrl(parent=self)
for i,j in enumerate('abcdef'):
self.list.InsertStringItem(i,j)
self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self.OnClick, self.list)
self.Layout()
def OnClick(self, event):
print event.GetText()
if __name__ == '__main__':
app = wx.App(redirect=False)
frame = MainWidget(None, "ListCtrl Test")
frame.Show(True)
app.MainLoop()
This app will print the item in the ListCtrl that is activated (by pressing enter or double-clicking). If you just want to catch a single click event, you could use wx.EVT_LIST_ITEM_SELECTED.
The important point is that the Bind function specifies the method to be called when a particular event happens. See the section in the wxPython Getting Started guide on event handling. Also see the docs on ListCtrl for the events that widget uses.

pyqt graphicsitem mouse enter event

I am trying to connect to the mouse-enter event of QGraphicsItems that are placed onto a QGraphicsScene and visualized through a QGraphicsView. From what I understand, the method to override for this is dragEnterEvent in a class derived from QGraphicsItem (or one of it's subclasses). My attempt looks like this:
class StaPoly(QtGui.QGraphicsPolygonItem):
def __init__(self,*args):
QtGui.QGraphicsPolygonItem.__init__(self,*args)
self.setAcceptDrops(True)
def dragEnterEvent(self,event):
print "Enter!"
...
def draw(self):
p = self.parent
self.group = QtGui.QGraphicsItemGroup(scene=p.scene)
...
for xpix in lons:
poly = QtGui.QPolygonF()
poly << QtCore.QPointF(xpix-symw,ypix)
poly << QtCore.QPointF(xpix,ypix+symh)
poly << QtCore.QPointF(xpix+symw,ypix)
poly << QtCore.QPointF(xpix,ypix-symh)
item = StaPoly(poly)
item.setPen(QtGui.QColor(color))
item.setBrush(QtGui.QColor(color))
self.group.addToGroup(item)
I hope the above snippets make it clear what I am trying to do. Note that the display is generated exactly as I desire, no issue there - however the polygons that get drawn are not responding to the enter-event - I am not seeing any evidence that dragEnterEvent() is being called.
The (partial) solution was this:
self.group.setHandlesChildEvents(False)
This ensures that individual items handle their own events, it seems that before the group was capturing them.
I still have a problem in that my GraphicsView overrides the mouseMoveEvent, and when enabled, no events get propagated to scene items.
EDIT: And it seems the solution to this second problem was to call the base class mouseMoveEvent handler from the Views mouseMoveEvent handler e.g.
def mouseMoveEvent(self,event):
QtGui.QGraphicsView.mouseMoveEvent(self, event)

Resources