tkinter "scrolledtext" copy paste doesn't work reliably - python

I have found a behaviour which seems to be a bug in tkinter.
If you run the following (minimal to reproduce the bug) code:
import tkinter, tkinter.simpledialog, tkinter.scrolledtext
root = tkinter.Tk('test')
text = tkinter.scrolledtext.ScrolledText(master=root, wrap='none')
text.pack(side="top", fill="both", expand=True, padx=0, pady=0)
text.insert(tkinter.END, 'abc\ndef\nghi\nijk')
root.mainloop()
then:
select one row in the scrolledtext widget, e.g. the row "ghi",
copy it with CTRL+C
do nothing else and close the app
Then paste it (CTRL+V) in any other Windows app: it won't work, nothing will be pasted. Why?
How to solve this?
Note: the expected behaviour is that text copied with CTRL+C should persist in the clipboard even if the app is closed. This is the default behaviour in many Windows software. Example here with notepad.exe:
link to the animated screen capture: https://i.imgur.com/li7UvYw.mp4
Note: this is linked to
https://bugs.python.org/issue23760
Tkinter in Python 3.4 on Windows don't post internal clipboard data to the Windows clipboard on exit
Tk only copies to clipboard if "paste" is used before program exits
https://core.tcl-lang.org/tk/tktview/1844034fffffffffffff
etc.

You can also use pyperclip which supports Windows, Linux and Mac
import tkinter as tk
import pyperclip
def copy(event:tk.Event=None) -> str:
try:
text = text_widget.selection_get()
pyperclip.copy(text)
except tk.TclError:
pass
return "break"
root = tk.Tk()
text_widget = tk.Text(root)
text_widget.pack()
text_widget.bind("<Control-c>", copy)
root.mainloop()

For a Windows only solution try this:
import tkinter as tk
import os
def copy(event:tk.Event=None) -> str:
try:
# Get the selected text
# Taken from: https://stackoverflow.com/a/4073612/11106801
text = text_widget.selection_get()
# Copy the text
# Inspired from: https://codegolf.stackexchange.com/a/111405
os.system("echo.%s|clip" % text)
print(f"{text!r} is in the clipboard")
# No selection was made:
except tk.TclError:
pass
# Stop tkinter's built in copy:
return "break"
root = tk.Tk()
text_widget = tk.Text(root)
text_widget.pack()
text_widget.bind("<Control-c>", copy)
root.mainloop()
Basically I call my own copy function whenever the user presses Control-C. In that function I use the clip.exe program that is part of the OS to copy the text.
Note: my approach to copying data to the clipboard using os.system, isn't great as you can't copy | characters. I recommend looking here for better ways. You just need to replace that 1 line of code.

Using pyperclip and root.bind_all() we can solve the problem.
import tkinter, tkinter.simpledialog, tkinter.scrolledtext
import pyperclip as clip
root = tkinter.Tk('test')
text = tkinter.scrolledtext.ScrolledText(master=root, wrap='none')
def _copy(event):
try:
string = text.selection_get()
clip.copy(string)
except:pass
root.bind_all("<Control-c>",_copy)
text.pack(side="top", fill="both", expand=True, padx=0, pady=0)
text.insert(tkinter.END,'abc\ndef\nghi\njkl')
root.mainloop()

Related

tkinter: Copy to clipboard via button

The idea of the code is to create N amount of buttons that copy text to the clipboard when pressed, overwriting and saving the text from the last pressed button.
from tkinter import *
import tkinter
r = Tk()
age = '''
O.o
giga
'''
gage = 'vrum'
r.title("getherefast")
def gtc(dtxt):
r.withdraw()
r.clipboard_clear()
r.clipboard_append(dtxt)
r.update()
tkinter.Button(text='age', command=gtc(age)).grid(column=1, row=0)
tkinter.Button(text='gage', command=gtc(gage)).grid(column=2, row=0)
r.mainloop()
With this code I expected to get 2 buttons 'age' and 'gage' and when I press them to get respectively the value saved in the var.
The problem is that the tkinter UI does not load and the Idle window is just standing open.
The result is that I get 'vrum' copied to the clipboard (If age button is the only 1 present I get the correct value but still no GUI from tkinter).
As additional information I'm writing and testing the code in IDLE, Python 3.10.
The problem is that the tkinter UI does not load
Yes it does, but you told it to withdraw(), so you don't see it.
To do this you need a partial or lambda function, you can't use a normal function call in a command argument. Try this:
import tkinter
r = tkinter.Tk()
age = '''
O.o
giga
'''
gage = 'vrum'
r.title("getherefast")
def gtc(dtxt):
r.clipboard_clear()
r.clipboard_append(dtxt)
tkinter.Button(text='age', command=lambda: gtc(age)).grid(column=1, row=0)
tkinter.Button(text='gage', command=lambda: gtc(gage)).grid(column=2, row=0)
r.mainloop()

How can I make a simple tkinter input box that creates a file with user input as name using os?

I'm trying to make tkinter input box to create a file using os' "touch". Code attached down below. Instead of creating the named file, it gives me the following message:
TypeError: cannot concatenate 'str' and 'instance' objects
I have tried the os.system lines with # before them, but it did nothing. Could anyone please tell me what I need to fix to get this working?
My operating system is MacOS if that is important.
from tkinter import *
import os
os.system("clear")
root = tk.Tk()
def createFile():
#os.system("cd ~")
os.system("touch" + e1)
#os.system.pack()
e1 = Entry(root)
e1.pack()
button1 = Button(root, text="Create File", command=createFile)
button1.pack()
root.mainloop()```
The reason is that you are trying to concatenate 'touch' string with an Entry object. You should get the entry text first by using .get method. Also, there should be a space after touch probably, i.e. touch . And in general, why do you need system? For example, in Windows the touch command would fail. There is open function in Python for creating and reading files. Also I would recommend not to use * in import. A linter (for example pylint) would complain about this approach.
The code I suggest is:
import tkinter as tk
import os
os.system("clear")
root = tk.Tk()
def createFile():
# os.system("touch " + e1.get())
# I suggest this:
file = open(e1.get(), 'w')
file.close()
e1 = tk.Entry(root)
e1.pack()
button1 = tk.Button(root, text="Create File", command=createFile)
button1.pack()
root.mainloop()

Redirect my console output to my Tkinter text area widget

I'm a newbie on tkinter, my code can run but I need my text widget to display only the result variable in the callback() function not including the 'askopenfilename' method.
from Tkinter import *
from tkFileDialog import *
import os
root = Tk()
root.geometry('900x700')
path = StringVar()
#browse pdf files
def callback():
f = askopenfilename(title='Open Files',initialdir='C:\Users\shantini\Desktop\PDF',
filetypes=[('Files of type:','*.PDF'),('Files of type:','*.pdf')])
path.set(f)
result = os.popen('pdfid.py'+' '+f).read()
return result
#labelframe(text pdf output)
label=LabelFrame(root, text="PDF Analysis Output")
label.pack(side=BOTTOM, anchor=W, fill=BOTH, expand=YES)
text = Text(label,bg='white')
text.pack(fill=BOTH, expand=YES)
text.insert(INSERT,callback())
root.mainloop()
If you disable Text widget, you make it read-only, so you cant add text to it. So to add text, make it normal, or remove the state parameter. I changed your callback to reflect the comments:
def callback():
f = askopenfilename(title='Open Files',initialdir='/tmp',
filetypes=[('Files of type:','*.PDF'),('Files of type:','*.pdf')])
result = open(f).read() # I also changed this as I dont have `pdfid.py` to test the code
text_area.insert(INSERT, result)
print result
I slightly change the input files and folder, as I work in linux and cant use windows paths. Hope this helps.

Problems using Tkinter askopenfile

I want to launch an "Open File" dialog in Tkinter in Python 2.7.
My code starts with:
from Tkinter import Frame, Tk, BOTH, Text, Menu, END
import tkFileDialog as tkfd
import fileinput
root = Tk()
global strTab
strTab = ""
def openTab(event):
r = tkfd.askopenfilename()
strTab = unicodedata.normalize('NFKD', r).encode('ascii','ignore')
Later in the code I have:
btnLoadTab = Button(root,
text="Load Tab",
width=30,height=5,
bg="white",fg="black")
btnLoadTab.bind("<Button-1>", openTab)
btnLoadTab.pack()
root.mainloop()
When I press the button an "Open File" dialog is shown, but when I select a file it closes and the button remains "clicked".
If I later call to strTab outside of openTab, it remains equal to "".
You can find workable example here: http://www.python-course.eu/tkinter_dialogs.php

Disabling Copy/Paste action from the Tkinter entry

How can I disable the copying and pasting on the tkinter entry field. I tried using:
self.ent_city = Tkinter.Entry(bd='4',width='32', state='readonly')
But this command is nearly similar to state = 'disabled' . I want to disable Copy/paste or Cut/Paste on my entry widget.
Any help would be appreciated.!
Thanks in advance
You can bind key-presses and the right-button click like this:
from Tkinter import *
class App(Frame):
def __init__(self):
Frame.__init__(self)
self.pack()
self.ent = Entry(self, width=15)
self.ent.pack()
self.ent.bind('<Control-x>', lambda e: 'break') #disable cut
self.ent.bind('<Control-c>', lambda e: 'break') #disable copy
self.ent.bind('<Control-v>', lambda e: 'break') #disable paste
self.ent.bind('<Button-3>', lambda e: 'break') #disable right-click
root = Tk()
app = App()
mainloop()
It's not fool-proof, but I think it's a decent solution. Check http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm for more info.
I don't think there's an official way to disable cut/copy/paste in tkinter (very annoying), and calling self.ent.bind('<Control-v>', lambda e:'break') doesn't seem to work on XWayland (tkinter runs in x11 on Xorg, or XWayland on Wayland)
I've tried self.ent.unbind('<<paste>>') but this is no help either.
A bodge that seems to work is to bind your own function to paste, in it, empty the clipboard into a string, then fill it again after a 20ms timeout, this way, when tkinter pastes, it pastes nothing. This isn't ideal though as any image or html on the clipboard will get mushed up. Also, you have to catch the error if the clipboard is empty.
self.ent.bind('<Control-v>', self.paste)
def paste(self, event):
try: s = self.ent.clipboard_get()
except: s = ''
self.ent.clipboard_clear()
#any other code you want to run on paste here
self.ent.after(20, lambda: self.ent.clipboard_append(s))

Categories