I am trying to put a module in my program that is able to get a string inputted in an entry box and search it in an existing csv file within the same directory. after that, to see if the string has been found, it should either print found or not found. its currently showing the error:
TypeError: 'type' object is not subscriptable
from tkinter import *
from tkinter import ttk
import csv
import re
import os
win= Tk()
win.resizable(0, 0)
win.title('PRODUCT QUERY')
text_input=StringVar()
int_input1=IntVar()
int_input1.set('')
int_input2=IntVar()
int_input2.set('')
def update():
import product_updater
def searcher():
with open('products_database.csv', 'r') as x:
global word
word=str[int_input1]
y=x.readlines()
dbase_list=list(y)
for i in word:
if re.search(i, dbase_list):
print('found')
else:
print('not found')
a=Label(win, text='Scan barcode').grid(column=0, row=0)
b=Entry(win, text=int_input1).grid(column=1, row=0)
c=Label(win, text='Item').grid(column=0, row=1)
d=Entry(win, text=text_input).grid(column=1, row=1)
e=Label(win, text='Sale price').grid(column=0, row=2)
f=Entry(win, text=int_input2).grid(column=1, row=2)
g=Button(win, text='Verify', command=searcher, width=20).grid(column=0, row=3, columnspan=1)
h=Button(win, text='Add product', command=update, width=20).grid(column=1, row=3)
win.mainloop()
As Andre suggested, pandas is a great library for .csv manipulations.
In this case, to find if a string is included within the table, you can do:
import pandas as pd
def find_string_csv(file_name, to_find):
my_csv = pd.read_csv(file_name)
for col in my_csv:
if my_csv[col].str.contains(to_find).any():
print('found it')
break
else:
print('not found')
The .contains method runs a regex matching option by default. This code will iterate over each columns and check if the string is present in any of them. If it is, it will break after the first occurrence and print found it.
Note that it will not match the headers of the columns, so if you have a column named 'abc', running this function with to_find == 'abc', it will not find it. If you want to include the column names in the query, add header=None in your read_csv() parameters.
Additionally, if you prefer one liners, you can replace the whole for / else loop with:
print('found it') if my_csv.isin([to_find]).any().any() else print('not found')
The isin will return a new pd.Dataframe with boolean values of True if equals to to_find and False otherwise. Calling any on it will give you a pd.Serie where each entry will be a boolean representing whether a given column has a True inside. The second any will give you a boolean of True if any column is True, False otherwise.
I find the for loop clearer and easier to read though.
Related
I have a code that works and have a simple question. If I were to change the text-file from strings to integer I want to error handle that. Is it possible? I only get error-warning when I change the integers to strings however I want it to work for changing strings to integer. For example if I change "Football" into a integer I want to get a error-warning. I then want to create an error-handling to print for example: "Something is wrong inside the textfile"
textfile:
Football # 8-9 # Pitch
Basketball # 9-10 # Gym
Lunch # 11-12 # Home
Reading # 13-14 # Library
from pprint import pprint
class Activity:
def __init__(self, name, start_time, end_time, location):
self.name = str(name)
self.start = int(start_time)
self.end = int(end_time)
self.location = str(location)
def read_file(filename):
activities = []
with open(filename, 'r') as f:
for line in f:
activity, time, location = line.strip().split(' # ')
start, end = time.split('-')
activities.append(Activity(activity, start, end, location))
return activities
activities = read_file('sample.txt')
pprint(activities)
You can check if a string is an integer by using isnumeric.
So let's change the input file to:
Football # 8-9 # Pitch
Basketball # 9-10 # Gym
Lunch # 11-12 # Home
3 # 13-14 # Library
Now we want to verify the name, for which we will write a validator:
class Activity:
def __init__(self, name, start_time, end_time, location):
self.name = self.validate_name(name)
self.start = int(start_time)
self.end = int(end_time)
self.location = location
def validate_name(self, name):
if name.isnumeric():
raise TypeError(f"The name: {name!r}, is not a string, please check your input file.")
return name
def __repr__(self):
return f"<{type(self).__name__}, (name={self.name}, start={self.start}, end={self.end}, loc={self.location})>"
Which results in a TypeError:
line 13, in validate_name
raise TypeError(f"The name: {name!r}, is not a string, please check your input file.")
TypeError: The name: '3', is not a string, please check your input file.
Note that your input is already a string, so there was no need to use str(name) and str(location).
Edit
The above solution only verifies if the entire name is an integer. For a solution that checks if the input is using valid characters we can use the re module in python and the method:
import re
def validate_input(self, name):
regex = re.compile('\d')
if regex.match(name):
raise TypeError(f"The name: {name!r}, contains an integer, please check your input file.")
return name
This will break whenever there is an integer in the input name. The previous solution would continue on inputs such as: Football 3, 3 Football. This solution would raise an error.
You can try out the regex expression for yourself on regex101
I have the following code :
from tkinter import *
class GUI:
def __init__(self,master):
self.ip_word = Label(master,text="Input Path")
self.ip_word.grid(row=0,sticky=E)
self.ip_path_field = Entry(master)
self.ip_path_field.grid(row=0,column=1,sticky=W)
self.op_word = Label(master,text="Output Path")
self.op_word.grid(row=2,sticky=E)
self.op_path_field = Entry(master)
self.op_path_field.grid(row=2,column=1,sticky=W)
self.filename_word=Label(master,text="Output Filename ")
self.filename_word.grid(row=4,sticky=E)
self.filename =Entry(master)
self.filename.grid(row=4,column=1,sticky=W)
self.Submit = Button(master,text="Submit",fg="black",bg="white",command=self.Scraper(ip_path_field,op_path_field,filename) )
self.Submit.grid(row=5,columnspan=2)
"""
def printMessage(self):
str1=ip_path_field
str2=op_path_field
str3=filename
Scraper(str1,str2,str3)"""
def Scraper(self,ip_path_field,op_path_field,filename):
import pandas as pd
import os
# "C:/Users/chowdhuryr/Desktop/first automation/MAIN RESEARCH DATA.xlsx"
user_input =ip_path_field#input("Enter the input file path of your file: ")
if os.path.exists(user_input):
df = pd.read_excel(user_input, sheetname='Sheet1')
print("File Found and We are Processing !")
else:
print ("Input Directory does not exists.")
#"C:/Users/chowdhuryr/Desktop/first automation/OUTPUT DATA.xlsx"
user_output =op_path_field#input("Enter the output file path of your file: ")
#if os.path.exists(user_input):
#df = pd.read_excel(user_input, sheetname='Sheet1')
#--------------------------------------------------------------------------------------------------------------------------------------
#setting up the path
import os
os.chdir('C:/Users/chowdhuryr/Desktop/first automation')
df=df[0:5]
#--------------------------------------------------------------------------------------------------------------------------------------
#importing necessary packages
from selenium import webdriver
#from selenium.webdriver.common.keys import Keys
from selenium.common.exceptions import NoSuchElementException
#---------------------------------------------------------------------------------------------------------------------------------------
#Setting up Chrome webdriver
#chrome_options = webdriver.ChromeOptions()
options = webdriver.ChromeOptions()
options.add_argument("--headless") #making the window work in the background
options.add_argument('window-size=1200x850')
#declaring a list to store the messages
Message=list()
Tier=list()
Wirecentre=list()
#---------------------------------------------------------------------------------------------------------------------------------------
#iteration to access the url and and retriving the Tier Locations
for i in range(0,df.shape[0]): #(0,df.shape[0]):
driver = webdriver.Chrome(executable_path='C:/Users/chowdhuryr/Desktop/first automation/chromedriver', chrome_options=options) #openning chrome
#driver.maximize_window() #maximizing the window
driver.get('https://clec.att.com/facilitiescheck/facilities_chk.cfm') #openning the url
street_address=driver.find_element_by_xpath('/html/body/form/table/tbody/tr[2]/td[2]/table/tbody/tr[1]/td[2]/input') #accessing the street address field
street_address.send_keys(df['CIRCUIT_LOC_ADDR'][i].split(',')[0]) #passing the values to street_address location
city=driver.find_element_by_xpath('/html/body/form/table[1]/tbody/tr[2]/td[2]/table/tbody/tr[2]/td[2]/input') #accessing the city to street address field
city.send_keys(df['CIRCUIT_LOC_ADDR'][i].split(',')[1]) #passing the values to the city location
state=driver.find_element_by_xpath('/html/body/form/table[1]/tbody/tr[2]/td[2]/table/tbody/tr[4]/td[2]/select') #accessing the state field
state.send_keys(df['CIRCUIT_LOC_ADDR'][i].split(',')[2]) #passing the values to the state field
checkbox=driver.find_element_by_xpath('/html/body/form/table[1]/tbody/tr[2]/td[2]/table/tbody/tr[8]/td[1]/input') #accessing the checkbox
checkbox.click() #clicking on the check box
search_button=driver.find_element_by_xpath('/html/body/form/table[1]/tbody/tr[2]/td[2]/table/tbody/tr[8]/td[1]/input') #accessing the submit button
search_button.submit() #clicking the submit button
#try-except block in case if radio button appears
try:
Address=driver.find_element_by_xpath('/html/body/form/table[2]/tbody/tr[1]/td[2]/b') #taking the xpath of the address block
if (Address): #checking if it contains any radio button or not
Radio_button=driver.find_element_by_xpath('/html/body/form/table[2]/tbody/tr[2]/td[1]/input') #getting the xpath of radio button
Radio_button.click() #clicking the radio button
submit_button=driver.find_element_by_xpath('/html/body/form/table[3]/tbody/tr[2]/td/input') #getting the submit button
submit_button.submit()
except NoSuchElementException:
print('no such element found')
message_body= driver.find_element_by_xpath('//*[#id="msg"]/table/tbody/tr/td').text #Extracting the Message from the message box
Message.append(message_body[14:]) #putting the message into a text
str = message_body.split() #splitting the message
if any ("Tier"in s for s in str):
j=str.index('Tier')
Tier.append(str[j+1])
else:
Tier.append("NULL")
if any ("AT&T"in s for s in str):
j=str.index('AT&T')
Wirecentre.append(str[j+1])
else:
Wirecentre.append("NULL")
#saving the screenshot
str=df['STRIP_EC_CIRCUIT_ID'][i]
filename="C:\\Users\\chowdhuryr\\Desktop\\first automation\\"+str+".png" #Taking the circuit id name
driver.get_screenshot_as_file(filename)
driver.close() #closiing the driver
#------------------------------------------------------------------------------------------------------------------------------------------
#putting the back thenew columns into the dataframe and storing it into an excel sheet
df['Tier']=Tier #putting the Tier column back into the dataset
df['Wirecentre']=Wirecentre #putting the Wirecentre column back into the dataset
df['Message']=Message #putting the Message column back into the dataset
if os.path.exists(user_output):
user_output="user_output"+filename+".xlsx"
writer = pd.ExcelWriter(user_output) #writing the dataframe down into a new excel file
df.to_excel(writer,'sheet1',index=False) #to_excel(writer,'Sheet1')
writer.save()
else:
print ("Output Directory does not exists.")
#-------------------------------------------------------------------------------------------------------------------------------------------
#Generating pop up window at the end of the process
popup_driver=webdriver.Chrome()
popup_driver.maximize_window()
popup_driver.execute_script(" window.alert('Process is Completed');") #generating the pop up """
root =Tk()
b=GUI(root)
#b.Scraper(ip_path_field,op_path_field,filename)
root.mainloop()
Now what I want to do is this :
I want to pass the variables ip_path_field,op_path_field,filename as arguments to the function named scraper . Now ip_path_field,op_path_field,filename are all user inputs and not hard coded strings. Now whenever I run the following code, I get the GUI opened and whenever I provide my inputs in the required edit boxes and press the submit button I get the following error name 'ip_path_field' is not defined. My purpose of this code is to pass the user defined file paths to the function called scraper() as defined in the code above.
You need to create a callback that gets the values, and then passes them to the function that does the work.
Example:
class GUI:
def __init__(self,master):
...
self.Submit = Button(..., command=self.handle_submit)
...
def handle_submit(self):
ip_path = self.ip_path_field.get()
op_path = self.op_path_field.get()
filename = self.filename.get()
self.Scrapper(ip_path_field,op_path_field,filename)
The main problem you are having is related to how you built your method. Instead you should have put the get() calls inside that method. This way you can simple run the command to call scraper without needing to pass arguments. I have taken your code and re-written it to more closely follow the PEP8 standard with the method correction included.
You only need to apply the self. prefix to class attributes/methods. Things like labels/buttons that are not going to be modified later in the code should be left as normal widgets that are not assigned as attributes.
Next I moved all your imports to the top of the code. You only need to import a library one time and it should all be listed at the top of your code. On imports keep in mind it is better to import tkinter as tk instead of using * to prevent any accidental overrides occurring.
I have changed your string concatenation to use the format() method as + is deprecated.
Here is your code with some basic clean up as well.
import tkinter as tk
import pandas as pd
import os
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException
class GUI:
def __init__(self,master):
tk.Label(master, text="Input Path").grid(row=0, sticky="e")
tk.Label(master, text="Output Path").grid(row=2, sticky="e")
tk.Label(master, text="Output Filename").grid(row=4, sticky="e")
self.ip_path_field = tk.Entry(master)
self.op_path_field = tk.Entry(master)
self.filename = tk.Entry(master)
self.ip_path_field.grid(row=0, column=1, sticky="w")
self.op_path_field.grid(row=2, column=1, sticky="w")
self.filename.grid(row=4, column=1, sticky="w")
tk.Button(master, text="Submit", fg="black", bg="white",
command=self.scrapper).grid(row=5,columnspan=2)
def scrapper(self): # changed function to a method.
user_input = self.ip_path_field.get(0, "end")
user_output = self.op_path_field.get(0, "end")
filename = self.filename.get(0, "end")
if os.path.exists(user_input):
df = pd.read_excel(user_input, sheetname='Sheet1')
print("File Found and We are Processing !")
else:
print ("Input Directory does not exists.")
os.chdir('C:/Users/chowdhuryr/Desktop/first automation')
df = df[0:5]
options = webdriver.ChromeOptions()
options.add_argument("--headless")
options.add_argument('window-size=1200x850')
message = list()
tier = list()
wirecentre = list()
for i in range(0,df.shape[0]):
driver = webdriver.Chrome(executable_path='C:/Users/chowdhuryr/Desktop/first automation/chromedriver', chrome_options=options)
driver.get('https://clec.att.com/facilitiescheck/facilities_chk.cfm')
street_address = driver.find_element_by_xpath('/html/body/form/table/tbody/tr[2]/td[2]/table/tbody/tr[1]/td[2]/input')
street_address.send_keys(df['CIRCUIT_LOC_ADDR'][i].split(',')[0])
city=driver.find_element_by_xpath('/html/body/form/table[1]/tbody/tr[2]/td[2]/table/tbody/tr[2]/td[2]/input')
city.send_keys(df['CIRCUIT_LOC_ADDR'][i].split(',')[1])
state = driver.find_element_by_xpath('/html/body/form/table[1]/tbody/tr[2]/td[2]/table/tbody/tr[4]/td[2]/select')
state.send_keys(df['CIRCUIT_LOC_ADDR'][i].split(',')[2])
checkbox = driver.find_element_by_xpath('/html/body/form/table[1]/tbody/tr[2]/td[2]/table/tbody/tr[8]/td[1]/input')
checkbox.click()
search_button = driver.find_element_by_xpath('/html/body/form/table[1]/tbody/tr[2]/td[2]/table/tbody/tr[8]/td[1]/input')
search_button.submit()
try:
address = driver.find_element_by_xpath('/html/body/form/table[2]/tbody/tr[1]/td[2]/b')
if (address):
radio_button = driver.find_element_by_xpath('/html/body/form/table[2]/tbody/tr[2]/td[1]/input')
radio_button.click()
submit_button = driver.find_element_by_xpath('/html/body/form/table[3]/tbody/tr[2]/td/input')
submit_button.submit()
except NoSuchElementException:
print('no such element found')
message_body = driver.find_element_by_xpath('//*[#id="msg"]/table/tbody/tr/td').text
message.append(message_body[14:])
strx = message_body.split()
if any ("Tier"in s for s in strx):
j = strx.index('Tier')
tier.append(strx[j+1])
else:
tier.append("NULL")
if any ("AT&T"in s for s in strx):
j = strx.index('AT&T')
wirecentre.append(strx[j+1])
else:
wirecentre.append("NULL")
strx = df['STRIP_EC_CIRCUIT_ID'][i]
filename = "C:\\Users\\chowdhuryr\\Desktop\\first automation\\{}.png".format(strx)
driver.get_screenshot_as_file(filename)
driver.close()
df['Tier'] = tier
df['Wirecentre'] = wirecentre
df['Message'] = message
if os.path.exists(user_output):
user_output="user_output{}.xlsx".format(filename)
writer = pd.ExcelWriter(user_output)
df.to_excel(writer, 'sheet1', index=False)
writer.save()
else:
print ("Output Directory does not exists.")
popup_driver = webdriver.Chrome()
popup_driver.maximize_window()
popup_driver.execute_script(" window.alert('Process is Completed');")
root = tk.Tk()
b = GUI(root)
root.mainloop()
All that said you should try to use a minimal code next time. It is easier to fix issues when you narrow it down to exactly the problem. For example based on your question a Minimal, Complete, and Verifiable example would look something like this.
from tkinter import *
from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException
class GUI:
def __init__(self,master):
self.ip_path_field = Entry(master)
self.ip_path_field.grid(row=0,column=1,sticky=W)
self.op_path_field = Entry(master)
self.op_path_field.grid(row=2,column=1,sticky=W)
self.filename =Entry(master)
self.filename.grid(row=4,column=1,sticky=W)
self.Submit = Button(master, text="Submit",
command=self.Scrapper(ip_path_field,op_path_field,filename) )
self.Submit.grid(row=5,columnspan=2)
def Scrapper(self,ip_path_field,op_path_field,filename):
user_input = ip_path_field
user_output = op_path_field
filename = filename
root = Tk()
b = GUI(root)
root.mainloop()
I am displaying data from an SQLite database in a QTableView using a QSqlTableModel. Letting the user edit this data works fine. However, for some columns I want to use QComboboxes instead of free text cells, to restrict the list of possible answers.
I have found this SO answer and am trying to implement it on my model/view setting, but I'm running into problems (so this is a follow-up).
Here's a full mini-example:
#!/usr/bin/python3
# -*- coding: utf-8 -*-
from PyQt5 import QtSql
from PyQt5.QtWidgets import (QWidget, QTableView, QApplication, QHBoxLayout,
QItemDelegate, QComboBox)
from PyQt5.QtCore import pyqtSlot
import sys
class ComboDelegate(QItemDelegate):
"""
A delegate that places a fully functioning QComboBox in every
cell of the column to which it's applied
source: https://gist.github.com/Riateche/5984815
"""
def __init__(self, parent, items):
self.items = items
QItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
combo = QComboBox(parent)
li = []
for item in self.items:
li.append(item)
combo.addItems(li)
combo.currentIndexChanged.connect(self.currentIndexChanged)
return combo
def setEditorData(self, editor, index):
editor.blockSignals(True)
# editor.setCurrentIndex(int(index.model().data(index))) #from original code
editor.setCurrentIndex(index.row()) # replacement
editor.blockSignals(False)
def setModelData(self, editor, model, index):
model.setData(index, editor.currentIndex())
#pyqtSlot()
def currentIndexChanged(self):
self.commitData.emit(self.sender())
class Example(QWidget):
def __init__(self):
super().__init__()
self.resize(400, 150)
self.createConnection()
self.fillTable() # comment out to skip re-creating the SQL table
self.createModel()
self.initUI()
def createConnection(self):
self.db = QtSql.QSqlDatabase.addDatabase("QSQLITE")
self.db.setDatabaseName("test.db")
if not self.db.open():
print("Cannot establish a database connection")
return False
def fillTable(self):
self.db.transaction()
q = QtSql.QSqlQuery()
q.exec_("DROP TABLE IF EXISTS Cars;")
q.exec_("CREATE TABLE Cars (Company TEXT, Model TEXT, Year NUMBER);")
q.exec_("INSERT INTO Cars VALUES ('Honda', 'Civic', 2009);")
q.exec_("INSERT INTO Cars VALUES ('VW', 'Golf', 2013);")
q.exec_("INSERT INTO Cars VALUES ('VW', 'Polo', 1999);")
self.db.commit()
def createModel(self):
self.model = QtSql.QSqlTableModel()
self.model.setTable("Cars")
self.model.select()
def initUI(self):
layout = QHBoxLayout()
self.setLayout(layout)
view = QTableView()
layout.addWidget(view)
view.setModel(self.model)
view.setItemDelegateForColumn(0, ComboDelegate(self, ["VW", "Honda"]))
for row in range(0, self.model.rowCount()):
view.openPersistentEditor(self.model.index(row, 0))
def closeEvent(self, e):
for row in range(self.model.rowCount()):
print("row {}: company = {}".format(row, self.model.data(self.model.index(row, 0))))
if (self.db.open()):
self.db.close()
def main():
app = QApplication(sys.argv)
ex = Example()
ex.show()
sys.exit(app.exec_())
if __name__ == '__main__':
main()
In this case, I want to use a QCombobox on the "Company" column. It should be displayed all the time, so I'm calling openPersistentEditor.
Problem 1: default values I would expect that this shows the non-edited field's content when not edited (i.e. the company as it is listed in the model), but instead it apparently shows the ith element of the combobox's choices.
How can I make each combobox show the model's actual content for this field by default?
Problem 2: editing When you comment out "self.fill_table()" you can check whether the edits arrive in the SQL database. I would expect that choosing any field in the dropdown list would replace the original value. But (a) I have to make every choice twice (the first time, the value displayed in the cell remains the same), and (b) the data appears in the model weirdly (changing the first column to 'VW', 'Honda', 'Honda' results in ('1', 'VW', '1' in the model). I think this is because the code uses editor.currentIndex() in the delegate's setModelData, but I have not found a way to use the editor's content instead. How can I make the code report the user's choices correctly back to the model? (And how do I make this work on first click, instead of needing 2 clicks?)
Any help greatly appreciated. (I have read the documentation on QAbstractItemDelegate, but I don't find it particularly helpful.)
Found the solution with the help of the book Rapid GUI Programming with Python and Qt:
createEditor and setEditorData do not work as I expected (I was misguided because the example code looked like it was using the text content but instead was dealing with index numbers). Instead, they should look like this:
def setEditorData(self, editor, index):
editor.blockSignals(True)
text = index.model().data(index, Qt.DisplayRole)
try:
i = self.items.index(text)
except ValueError:
i = 0
editor.setCurrentIndex(i)
editor.blockSignals(False)
def setModelData(self, editor, model, index):
model.setData(index, editor.currentText())
I hope this helps someone down the line.
I am trying to add these panda columns to a Listbox, so they read like this:
New Zealand NZD
United States USD
ETC.
I am using pandas to get the data from a .csv, but when I try and use a for loop to add the items to the list box using insert I get the error
NameError: name 'END' is not defined or
NameError: name 'end' is not defined
Using this code:
def printCSV():
csv_file = ('testCUR.csv')
df = pd.read_csv(csv_file)
print (df[['COUNTRY','CODE']])
your_list = (df[['COUNTRY','CODE']])
for item in your_list:
listbox.insert(end, item)
You could turn the csv file into a dictionary, use the combined country and currency codes as the keys and just the codes as the values, and finally insert the keys into the Listbox. To get the code of the current selection, you can do this: currencies[listbox.selection_get()].
listbox.selection_get() returns the key which you then use to get the currency code in the currencies dict.
import csv
import tkinter as tk
root = tk.Tk()
currencies = {}
with open('testCUR.csv') as f:
next(f, None) # Skip the header.
reader = csv.reader(f, delimiter=',')
for country, code in reader:
currencies[f'{country} {code}'] = code
listbox = tk.Listbox(root)
for key in currencies:
listbox.insert('end', key)
listbox.grid(row=0, column=0)
listbox.bind('<Key-Return>', lambda event: print(currencies[listbox.selection_get()]))
tk.mainloop()
I want to know where and what was changed by user in tkinter's Text widget.
I've found how to get that text was somehow modified by using <<Modified>>event but I can't get actual changes:
from tkinter import *
def reset_modified():
global resetting_modified
resetting_modified = True
text.tk.call(text._w, 'edit', 'modified', 0)
resetting_modified = False
def on_change(ev=None):
if resetting_modified: return
print ("Text now:\n%s" % text.get("1.0", END))
if False: # ????
print ("Deleted [deleted substring] from row %d col %d")
if False: # ????
print ("Inserted [inserted substring] at row %d col %d")
reset_modified()
resetting_modified = False
root = Tk()
text = Text(root)
text.insert(END, "Hello\nworld")
text.pack()
text.bind("<<Modified>>", on_change)
reset_modified()
root.mainloop()
For example, if I select 'ello' part from "hello\nworld" in Text widget then I press 'E', then I want to see
"Deleted [ello] from row 0 col 1" followed by "Inserted [E] at row 0 col 1"
is it possible to get such changes (or at least their coordinates) or I have basically to diff text on each keystroke if I want to detect changes run time?
Catching the low level inserts and deletes performed by the underlying tcl/tk code is the only good way to do what you want. You can use something like WidgetRedirector or you can do your own solution if you want more control.
Writing your own proxy command to catch all internal commands is quite simple, and takes just a few lines of code. Here's an example of a custom Text widget that prints out every internal command as it happens:
from __future__ import print_function
import Tkinter as tk
class CustomText(tk.Text):
def __init__(self, *args, **kwargs):
"""A text widget that report on internal widget commands"""
tk.Text.__init__(self, *args, **kwargs)
self._orig = self._w + "_orig"
self.tk.call("rename", self._w, self._orig)
self.tk.createcommand(self._w, self.proxy)
def proxy(self, command, *args):
# this lets' tkinter handle the command as usual
cmd = (self._orig, command) + args
result = self.tk.call(cmd)
# here we just echo the command and result
print(command, args, "=>", result)
# Note: returning the result of the original command
# is critically important!
return result
if __name__ == "__main__":
root = tk.Tk()
CustomText(root).pack(fill="both", expand=True)
root.mainloop()
After digging around, I've found that idlelib has WidgetRedirector which can redirect on inserted/deleted events:
from tkinter import *
from idlelib.WidgetRedirector import WidgetRedirector
def on_insert(*args):
print ("INS:", text.index(args[0]))
old_insert(*args)
def on_delete(*args):
print ("DEL:", list(map(text.index, args)))
old_delete(*args)
root = Tk()
text = Text(root)
text.insert(END, "Hello\nworld")
text.pack()
redir = WidgetRedirector(text)
old_insert=redir.register("insert", on_insert)
old_delete=redir.register("delete", on_delete)
root.mainloop()
Though it seems hacky. Is there a more natural way?