Python 3.6 tk ScrolledText on TopLevel won't insert - python

I am having issues with the code below when I use Pyinstaller to compile an executable.
I am attempting to open a second GUI window using TopLevel, adding a ScrolledText widget to it, then take the output of ipconfig /all and put it in the ScrolledText.
I stripped the code down to just the code relevant to this issue. If I run the python file directly, or compile with Pyinstaller without the --windowed command pyinstaller --onefile toolbox.py, then everything appears to work.
When I compile with --windowed pyinstaller --onefile --windowed toolbox.py, then the TopLevel window opens and I get the error message listed in the code and the ScrolledText widget is blank.
I have confirmed that using a normal string on the txt.insert command does work, so the issues appear to be with the os.popen command. I did attempt to use subprocess.check_output in place of os.popen but I get the same error message. Also, assigning the os.popen command to a variable then inserting the variable in txt.insert has the same result.
from tkinter import *
from tkinter import ttk
from tkinter import messagebox
from tkinter import scrolledtext
import os
import sys
def netIPInfo():
"""Open a second window, displays output of ipconfig /all."""
window2 = Toplevel()
window2.title("NIC IP Info")
window2.geometry("663x650")
txt = scrolledtext.ScrolledText(window2, width=80, height=40)
txt.pack(expand=1, fill="both")
try:
txt.insert(INSERT, os.popen("ipconfig /all").read()) # ISSUE!!
except:
e = sys.exc_info()
messagebox.showinfo("Error", e) # Error message below
# {<class 'OSError'}
# {[WinError 6] The handle is invalid}
# {<traceback object at 0x02EEB260>}
txt.configure(state="disabled")
window2.mainloop()
# Primary GUI Window
window = Tk()
window.title("ProTech QST")
tab_control = ttk.Notebook(window)
tab1 = ttk.Frame(tab_control)
tab_control.add(tab1, text='Network')
lfr5 = ttk.LabelFrame(tab1, text="Information")
lfr5.pack(fill=X)
btn30 = Button(lfr5, text="NIC IP Info")
btn30.config(width=18, command=netIPInfo)
btn30.grid(column=0, row=1, sticky=W)
tab_control.pack(expand=1, fill="both")
if __name__ == "__main__":
window.mainloop()
EDIT
I was playing with the code some more and found that if I run pyinstaller with --windowed but without --onefile it works. Same for running pyinstaller with --onefile but without --windowed. So is this an issue not with the code, but with pyinstaller?

Related

tkinter "scrolledtext" copy paste doesn't work reliably

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

Creating a .exe from a python script which runs a separate python script using pyinstaller

Short Version:
I have a series of python scripts that connect together (one .py closes and runs a separate .py). This works completely fine when running it through the terminal in VS Code or cmd line. Once it is in a .exe by pyinstaller, only the first code works and the program closes once it tries to execute a separate .py file.
Details:
All of the separate python files are saved in the same directory. The first one to open, 'Main.py', has a tkinter interface that allows the user to select which .py script they want to run. The code then closes the Main window and opens the selected python script using exec(open('chosen .py').read()). (This is a simplified version of the initial code but I am having the same issues)
import tkinter as tk
from tkinter import ttk
from tkinter.constants import W
from tkinter import messagebox as mb
""" Open a window to select which separate script to run"""
root = tk.Tk()
root.title('Selection Window')
root.geometry('300x200')
frame_1 = tk.LabelFrame(root, text='Choose Program')
frame_1.pack()
# Using this function to update on radio button select
def radio_button_get():
global program_int
choice = radio_ID.get()
if(choice == 1):
program_int = 1
elif(choice == 2):
program_int = 2
# Display confirmation popup
def run_script():
if(program_int == 1):
select = mb.askokcancel("Confirm", "Run choice 1?")
if(select == 1):
root.destroy()
else:
return
if(program_int == 2):
select = mb.askokcancel("Confirm", "No selection")
if(select == 1):
root.destroy()
else:
return
# Create radio buttons to select program
radio_ID = tk.IntVar()
radio_ID.set(2)
program_int = 2 # Set default selection
choice_1 = tk.Radiobutton(frame_1, text='Execute Script 1', variable=radio_ID, value=1, command=radio_button_get)
choice_1.pack()
no_choice = tk.Radiobutton(frame_1, text='No Selection', variable=radio_ID, value=2, command=radio_button_get)
no_choice.pack()
# Button to run the selected code
run_button = ttk.Button(root, text='Run', command=run_script)
run_button.pack()
root.mainloop()
# Execute the other python script
if(program_int == 1):
exec(open('Script1.py').read())
The next code is the 'Script1.py' file which 'Main.py' runs at the end. This is the step which works fine in VS Code and cmd line, but causes the .exe from pyinstaller to close.
import tkinter as tk
from tkinter import ttk
""" Create this programs GUI window"""
root = tk.Tk()
root.title('Script 1')
def run():
root.destroy()
label = ttk.Label(root, text='Close to run')
label.pack()
button = ttk.Button(root, text='Close', command=run)
button.pack()
root.mainloop()
""" Do some code stuff here"""
# When above code is done, want to return to the Main.py window
exec(open('Main.py').read())
Each independent .py file have been successfully turned into .exe files with pyinstaller previously. The cmd line command that I am using to execute pyinstaller is pyinstaller 'Main.py' This successfully creates a Main.exe in the dist folder and also includes a build folder.
I have read through pyinstallers documentation, but have not found anything that I believe would be useful in this case. The nearest issue I could find was importing python scripts as modules in the .spec file options but since the code executes the python script as a separate entity, I don't think this is the fix.
Would the issue be in how the scripts are coded and referencing each other, or with the installation process with pyinstaller? If I missed something in the documentation that would explain this issue, please let me know and I will look there!
Any help is greatly appreciated, thank you
We must avoid using the .exec command. It is hacky but unsafe. Ref: Running a Python script from another
Instead use import :
# Execute the other python script
if(program_int == 1):
import Script1
And here too:
# When above code is done, want to return to the Main.py window
import Main
That's it, now use pyinstaller.
EDIT:
Why .exe file fails to execute another script, and why exec() is the problem:
According to the documentation:
Pyinstaller analyzes your code to discover every other module and
library your script needs in order to execute. Then it collects copies
of all those files – including the active Python interpreter! – and
puts them with your script in a single folder, or optionally in a
single executable file.
So, when pyinstaller is analyzing & creating the .exe file, it only executes the exec() function that time (so no error thrown while pyinstaller runs), pyinstaller does not import it or copies it to your .exe. file, and then after the .exe file is created, upon running it throws error that no such script file exists because it was never compiled into that .exe file.
Thus, using import will actually import the script as module, when pyinstaller is executed, and now your .exe file will give no error.
Instead of importing the scripts as modules, for them to be re-executed again and again, import another script as a function in Main.py
Also, instead of destroying your Main root window (since you won't be able to open it again unless you create a new window), use .withdraw() to hide it and then .deiconify() to show.
First, in Script1.py:
import tkinter as tk
from tkinter import ttk
""" Create this programs GUI window"""
def script1Function(root): #the main root window is recieved as parameter, since this function is not inside the scope of Main.py's root
root2 = tk.Tk() #change the name to root2 to remove any ambiguity
root2.title('Script 1')
def run():
root2.destroy() #destroy this root2 window
root.deiconify() #show the hidden Main root window
label = ttk.Label(root2, text='Close to run')
label.pack()
button = ttk.Button(root2, text='Close', command=run)
button.pack()
root2.mainloop()
Then, in Main.py:
import tkinter as tk
from tkinter import ttk
from tkinter.constants import W
from tkinter import messagebox as mb
from Script1 import script1Function #importing Script1's function
# Execute the other python script
def openScript1():
root.withdraw() #hide this root window
script1Function(root) #pass root window as parameter, so that Script1 can show root again
""" Open a window to select which separate script to run"""
root = tk.Tk()
root.title('Selection Window')
root.geometry('300x200')
frame_1 = tk.LabelFrame(root, text='Choose Program')
frame_1.pack()
# Using this function to update on radio button select
def radio_button_get():
global program_int
choice = radio_ID.get()
if(choice == 1):
program_int = 1
elif(choice == 2):
program_int = 2
# Display confirmation popup
def run_script():
global program_int #you forgot to make it global
if(program_int == 1):
select = mb.askokcancel("Confirm", "Run choice 1?")
if(select == 1):
openScript1()
else:
return
if(program_int == 2):
select = mb.askokcancel("Confirm", "No selection")
if(select == 1):
root.destroy()
else:
return
# Create radio buttons to select program
radio_ID = tk.IntVar()
radio_ID.set(2)
program_int = 2 # Set default selection
choice_1 = tk.Radiobutton(frame_1, text='Execute Script 1', variable=radio_ID, value=1, command=radio_button_get)
choice_1.pack()
no_choice = tk.Radiobutton(frame_1, text='No Selection', variable=radio_ID, value=2, command=radio_button_get)
no_choice.pack()
# Button to run the selected code
run_button = ttk.Button(root, text='Run', command=run_script)
run_button.pack()
root.mainloop()

Tkinter GUI - Can't pass argument to script

I've made a GUI with Tkinter and i linked a script to a button. I've also created a browse file option in my GUI and when i select a file i store it's path into a variable named "file". What i'm trying to do is click the button and run the script using the path i stored into the variable "file", but i get a 'no such file or directory error'. The solution must be pretty obvious but i just can't figure it out. Here's my GUI code:
from tkinter import *
from tkinter import filedialog
from tkinter import ttk
from tkinter import messagebox
import subprocess
window = Tk()
#modify window
window.title("Random Title")
window.geometry("600x400")
tab_control = ttk.Notebook(window)
#Creating tabs
tab1 = ttk.Frame(tab_control)
tab2 = ttk.Frame(tab_control)
#Modifying tabs
tab_control.add(tab1, text='Issue')
tab_control.add(tab2, text='Verify')
file = ""
var = StringVar()
var.set("")
w = Entry(tab2,textvariable=var)
w.grid(column=1,row=0)
#Creating button & actions
def issue():
subprocess.call('./issue_script.sh', shell=True)
messagebox.showinfo('Issue Certificate', 'Certificate issued successfully!')
btn = Button(tab1, text="Issue Certificate", command=issue)
btn.grid(column=1, row=5)
def browse():
file = filedialog.askopenfilename(filetypes = (("all files","*.*"),("Text files","*.txt")))
var.set(file)
print(file)
btn2 = Button(tab2, text="Browse", command=browse)
btn2.grid(column=3, row=0)
def verify():
subprocess.call(['./verify_script.sh', file], shell=True)
btn = Button(tab2, text="Verify Certificate", command=verify)
btn.grid(column=1, row=5)
tab_control.pack(expand=1, fill='both')
#event loop
window.mainloop()
I've also added a print(file) command so that i see what is stored in the variable and i get the correct result(the path i selected). Maybe the error is in the line i call the script subprocess.call(['./verify_script.sh', file], shell=True) or in the script itself. Here's the script code:
#!/bin/bash
echo "Verifying certificate..."
cd
python3 cert-issuer/cert-verifier/cert_verifier/verifier.py $1
I actually made it work, but i don't know why it does.
All i changed was instead of calling my script like this
subprocess.call(['./verify_script.sh', var.get()], shell=True)
i omitted the shell=True command and the argument passes correctly into the script.
So i called subprocess.call(['./verify_script.sh', var.get()]) and it works just fine but i can't think why. Any explanation is much appreciated.

Python PyInstaller Tkinter Button Tied to Messagebox Does Nothing

I have a tiny button with a question mark in my app. When clicked, it displays the doc string from a function in my app which helps to explain what the app does.
The problem is, I build a single .exe using pyinstaller and this question mark button does nothing when clicked.
Recreation Steps
save the file below as run.py
open command-prompt
paste pyinstaller.exe --onefile --windowed run.py
go to dist folder and run single exe
click ? button and notice it does nothing
run.py below
import tkinter as tk
from tkinter import ttk
''' pyinstaller.exe --onefile --windowed run.py '''
def someotherfunction():
'''
This is some message that I
want to appear but it currently doesn\'t
seem to work
when I put try to launch a messagebox
showinfo window...
'''
pass
def showhelpwindow():
return tk.messagebox.showinfo(title='How to use this tool',
message=someotherfunction.__doc__)
root = tk.Tk()
helpbutton = ttk.Button(root, text='?', command=showhelpwindow, width=2)
helpbutton.grid(row=0, column=3, sticky='e')
root.mainloop()
My setup:
PyInstaller 3.2
Windows 7
Python 3.4.2
I tried adding --noupx option but that didn't fix it.
EDIT:
I removed --windowed option this time and the console is now showing me an error when I click this button.
Exception in Tkinter callback
Traceback (most recent call last):
File "tkinter\__init__.py", line 1533, in __call__
File "run.py", line 156, in showhelpwindow
AttributeError: 'module' object has no attribute 'messagebox'
Line 3 below is the solution. I needed to import the tkinter.messagebox module explicitly.
import tkinter as tk
from tkinter import ttk
import tkinter.messagebox
''' pyinstaller.exe --onefile --windowed run.py '''
def someotherfunction():
'''
This is some message that I
want to appear but it currently doesn\'t
seem to work
when I put try to launch a messagebox
showinfo window...
'''
pass
def showhelpwindow():
return tk.messagebox.showinfo(title='How to use this tool',
message=someotherfunction.__doc__)
root = tk.Tk()
helpbutton = ttk.Button(root, text='?', command=showhelpwindow, width=2)
helpbutton.grid(row=0, column=3, sticky='e')
root.mainloop()

How to hide console when displaying an image with Tkinter

I have a simple code block that displays an image when the user press a button. When I save the script with .py extension, there is console at the background so I decided to save it as .pyw to hide it. Here is my code;
from tkinter import *
from PIL import Image
def open_image():
im = Image.open("tobi.jpg")
im.show()
root = Tk()
root.geometry("800x600+400+300")
buton = Button(root)
buton.config(text = "Show the image", command = open_image, activebackground = "yellow", bg = "lightgreen")
buton.pack()
mainloop()
I don't see cmd at the background since I save it as .pyw. However, when I click the button, just before opening the image I see console for a short time, then it dissappears. How can I avoid this, I want to hide console completely,
I dont know how to do this through pure tk but if subprocess is an option then you can do the following. Not a very good solution, since you have to use the subprocess module just to open an image... but anyways....
import tkinter as tk
import subprocess as s
root = tk.Tk()
def open_image():
s.call(["tobi.jpg"], shell=True)
button = Button(root)
button.config(text="Show the Image", command=open_image, activebackground="yellow", bg="lightgreen")
button.pack()
root.mainloop()
alternatively you could use os and use os.system("start my_file_name.jpg")
(assuming windows here)
With os.system the console will show but only very briefly.
Really doing the same thing, opening with PIL is just slower than others, so you see the console for a longer period.
U can use the following code:`
import win32console
import win32gui
win=win32console.GetConsoleWindow() # For closing command window
win32gui.ShowWindow(win,0)
This will close the cmd window at start itself, and will not show again. The cmd window may show for a very little time at first, as a blink, then it will not show. u can save in .py extention.
here the full code:`
from Tkinter import *
from PIL import Image
import win32console
import win32gui
win=win32console.GetConsoleWindow() # For closing command window
win32gui.ShowWindow(win,0)
def open_image():
im = Image.open("tobi.jpg")
im.show()
root = Tk()
root.geometry("800x600+400+300")
buton = Button(root)
buton.config(text = "Show the image", command = open_image, activebackground = "yellow", bg = "lightgreen")
buton.pack()
mainloop()

Categories