4

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

enter image description here

Note: this is linked to

Basj
  • 41,386
  • 99
  • 383
  • 673
  • If you kill the `tkinter` window, all of the clipboard items that `tkinter` added get deleted on my computer. Also what is the point of `text.focus()`? Did you mean `text.focus_set()`? – TheLizzard Aug 28 '21 at 13:49
  • @TheLizzard I remove the `.focus()` part which was useless for this minimal example. More important: as an example, even if `notepad.exe` is closed, any copied text will be kept even if notepad.exe is closed. Example: https://imgur.com/li7UvYw This is the expected behaviour. Here with tkinter, it does not work. Any idea how to solve this? – Basj Aug 29 '21 at 10:53
  • See the edit at the end of the question for an example @TheLizzard. – Basj Aug 29 '21 at 11:09
  • @Basj That is just how `tkinter` works. I know it's very annoying and don't know why it is that way. You can instead bind to `Control-c` and use the [`clipboard`](https://pypi.org/project/clipboard/) library to copy the data. That should actually copy the data to the clipboard even if the `tkinter` window is closed. – TheLizzard Aug 29 '21 at 12:56
  • @TheLizzard I think this is a bug, as it breaks the expected behaviour and usual convention for all other apps (see notepad.exe as an example). – Basj Aug 30 '21 at 13:31
  • @Basj Well IDLE has/had the same [issue](https://bugs.python.org/issue40452). Also [sometimes](https://birdeatsbug.com/5-bugs-that-became-features) bugs become features. – TheLizzard Aug 30 '21 at 13:55
  • This issue seems OS-specific, I cannot reproduce this behavior on my computer (Linux with tk 8.6.11 and ClipIt clipboard manager) neither with the provided example nor with IDLE. – j_4321 Aug 30 '21 at 14:15
  • @j_4321 The issue that I linked also stated that it doesn't happen on all OSs due to the fact that tcl calls different functions for `TkClipCleanup` depending on the OS. Look at [this](https://bugs.python.org/msg369338) – TheLizzard Aug 30 '21 at 14:53
  • @Basj Do you just need a solution that will work on windows or do ou want it to be platform independent? If only on windows, try: `os.system("echo.%s|clip" % "The text to copy")`. Taken from [this](https://codegolf.stackexchange.com/a/111405) – TheLizzard Aug 30 '21 at 14:55
  • I had to deal with this before. I posted an answer a while back related this issue. Let me know if it helps. [tk-only-copies-to-clipboard-if-paste-is-used-before-program-exits](https://stackoverflow.com/questions/46178950/tk-only-copies-to-clipboard-if-paste-is-used-before-program-exits/46180493#46180493) – Mike - SMT Sep 04 '21 at 03:45
  • @Mike-SMT Thanks for the info! Have you tried the solution from https://bugs.python.org/issue23760? i.e. `r.after(100, r.destroy); r.mainloop()` to let Tkinter execute the code to copy to system clipboard on exit? I haven't tried yet, but it could be a workaround? Did it work for you? – Basj Sep 04 '21 at 09:24
  • You could do something like that but I have not tried. The only reason I ran into the problem originally was when I had copied something as a one off and tried to paste sometime after closing the app. I did not really need to consitantly paste when the app closes so I just went as far as to find a solution for my one off. – Mike - SMT Sep 04 '21 at 17:29

3 Answers3

4

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()
Chandan
  • 11,465
  • 1
  • 6
  • 25
  • 1
    Shouldn't you `return "break"` from the `copy` function? Also this will raise an error if the user hasn't selected anything. Also something interesting: `pyperclip` creates a new window on windows when you try coping. – TheLizzard Aug 30 '21 at 16:14
  • Also is the `pyperclip.paste()` needed? Doesn't it just return whatever is in the clipboard. From reading the library's source code I can see that it basically calls `ctypes.windll.user32.GetClipboardData` and converts it into a string. Otherwise nice answer – TheLizzard Aug 30 '21 at 22:47
  • @TheLizzard your last comment is useful: can we provide an answer for this question without using a third party dependance `pyperclip` nor your `echo ... | clip` solution, but just `ctypes` and this winAPI call? – Basj Nov 10 '21 at 13:34
  • @Basj Right now the Windows on my machine is very unstable so I can't test anything. I recommend looking at [this](https://github.com/asweigart/pyperclip/blob/781603ea491eefce3b58f4f203bf748dbf9ff003/src/pyperclip/__init__.py#L365). It has all of the code that you could need to implement it. – TheLizzard Nov 11 '21 at 16:37
1

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.

TheLizzard
  • 7,248
  • 2
  • 11
  • 31
  • Thank you for your answer. Would you know how is CTRL+C originally implemented in tkinter `scrolledtext`? I had a quick look in the source, but I didn't find it. Maybe a tweak there (that could be proposed as PR in Python core repo?) could work? Until this is solved, your solution is helpful indeed! – Basj Aug 31 '21 at 07:32
  • I looked in the source @TheLizzard, `scrolledtext` is a child class of `Text` https://github.com/python/cpython/blob/main/Lib/tkinter/__init__.py#L3561 but I found nothing about the handling of copy/paste there. – Basj Aug 31 '21 at 07:36
  • @Basj That is done in `tcl`. Basically `tcl` is a programming language to create GUIs. There is a module `_tkinter` which converts a lot of the `tcl` features into python (it's the C part of `tkinter`). Then there is `tkinter` which uses `_tkinter`. By returning `"break"` from my function, I tell `tcl` to not call the usual `Control-C` binding. – TheLizzard Aug 31 '21 at 09:02
1

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

Deli
  • 55
  • 7
  • Why are you using `bind_all`? I think that `bind_all` should be discouraged. Also this is the same as @Chandan's answer. The only difference is that you used `.bind_all` and a scrolled text (which wasn't in the question but ok). – TheLizzard Aug 30 '21 at 22:44
  • Also you import `simpledialog` without using it. Also why are you passing in `"test"` when creating `tkinter.Tk`? If you want to set the titlebar title for the window, use `root.title(....)`. – TheLizzard Aug 30 '21 at 22:50
  • My bad about the scrolled text not being needed but my other points still stand. – TheLizzard Aug 31 '21 at 09:03
  • Using bind on scrolledtext wont work and it shoul be bind_all on master And about import and title, i didnt change the question those were part of question. I just added the Function and bind_all and the rest were with the question. – Deli Aug 31 '21 at 10:58