pythontkinterpyqt5

Should I re-write my Tkinter in PyQt, or vice versa?


I wrote a Tkinter app, and I wanted to add screen snipping, so I found a separate program from GitHub (screen-snip) written in PyQt, which I was importing and using in my Tkinter app. Then I decided to combine the programs in order to ask an SO question about why they aren't totally working together. I've learned not to combine Tk and Qt.

So now my question is, should I rewrite my program in Qt, or Tk?

Which is better for this situation?

My currently mixed-Tk/Qt program works when you select an image file, but now the screen-snip portion with Qt class MyWidget(QtWidgets.QWidget): causes it to freeze and then crash.

enter image description here

I think the problem might be a result of mixing Qt with Tk, but I'm not sure. I originally had two instances of tkinter running, which allowed me to get the screen ship with a new window, but caused trouble with the button window, so I replaced this by trying to use tk.Toplevel

class MyWidget(QtWidgets.QWidget):
    def __init__(self, master):
        super().__init__()
        self.master = master
        self.window = tk.Toplevel(self.master)

and that's when I ran into trouble. The widget no longer works at all, and the program crashes without any clues or errors. Any idea why?

Simplified Code:

import tkinter as tk
from tkinter import filedialog
from PIL import ImageTk, Image, ImageGrab
import sys
from PyQt5 import QtWidgets, QtCore, QtGui
import numpy as np
import cv2


class ButtonImg:

    def __init__(self, master):
        self.newWindow = None
        self.master = master
        self.fontA = ("arial", 20, "bold")

        self.canvas = tk.Canvas(height = 5)
        self.canvas.pack()

        self.button = tk.Button(bg="#61B5DA", height = 5, text = "Select Image",
                                font = self.fontA, command = self.changeImage)
        self.button.pack(fill="both")

    def changeImage(self):
        print('open second window')
        self.newWindow = tk.Toplevel(self.master)
        img = AcquireImage(self.newWindow)
        self.master.wait_window(self.newWindow)
        print('close second window')

        if img.image_selected: # check if image was selected
            self.image = img.image_selected
            self.button.configure(image=self.image, height=self.image.height())


class AcquireImage:

    def __init__(self, master):
        self.master = master
        self.fontA = ("arial", 20, "bold")

        self.frame = tk.Frame(master, bg="#96beed")
        self.frame.pack(fill="both", expand=True)

        self.button1 = tk.Button(self.frame, text="Select Image File", padx=5, pady=5, bg="#6179DA",
                              font = self.fontA, command =lambda: self.show_dialogs(1))
        self.button1.grid(row=0, column=0, sticky="nsew")

        self.button2 = tk.Button(self.frame, text="Get Screen Snip", padx=5, pady=5, bg="#6179DA",
                              font = self.fontA, command=lambda: self.show_dialogs(2))
        self.button2.grid(row=0, column=1, sticky="nsew")

        self.image_selected = None

    def show_dialogs(self, method):

        if method == 1:
            ret = filedialog.askopenfilename() #filedialog.askopenfilename(initialdir='/home/user/images/')
            if ret:
                self.image_selected = ImageTk.PhotoImage(file = ret)
                self.master.destroy()

        elif method == 2:
            newWin = MyWidget(self.master)
            newWin.show()
            ret = newWin.img
            if ret:
                self.image_selected = ImageTk.PhotoImage(file = ret)


class MyWidget(QtWidgets.QWidget):
    def __init__(self, master):
        super().__init__()
        self.master = master
        self.window = tk.Toplevel(self.master)
        screen_width = self.thirdWin.winfo_screenwidth()
        screen_height = self.thirdWin.winfo_screenheight()
        self.setGeometry(0, 0, screen_width, screen_height)
        self.setWindowTitle(' ')
        self.begin = QtCore.QPoint()
        self.end = QtCore.QPoint()
        self.img = None

        self.setWindowOpacity(0.3)
        QtWidgets.QApplication.setOverrideCursor(
            QtGui.QCursor(QtCore.Qt.CrossCursor)
        )
        self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
        print('Capture the screen...')
        self.show()

    def getRect(self):
        # a commodity function that always return a correctly sized
        # rectangle, with normalized coordinates
        width = self.end.x() - self.begin.x()
        height = abs(width * 2 / 3)
        if self.end.y() < self.begin.y():
            height *= -1
        return QtCore.QRect(self.begin.x(), self.begin.y(),
                            width, height).normalized()

    def paintEvent(self, event):
        qp = QtGui.QPainter(self)
        qp.setPen(QtGui.QPen(QtGui.QColor('black'), 3))
        qp.setBrush(QtGui.QColor(128, 128, 255, 128))
        qp.drawRect(self.getRect())

    def mousePressEvent(self, event):
        self.begin = event.pos()
        self.end = self.begin
        self.update()

    def mouseMoveEvent(self, event):
        self.end = event.pos()
        self.update()

    def mouseReleaseEvent(self, event):
        self.close()

        rect = self.getRect()
        self.img = ImageGrab.grab(bbox=(
            rect.topLeft().x(),
            rect.topLeft().y(),
            rect.bottomRight().x(),
            rect.bottomRight().y()
        ))
        #self.img.save('capture.png')
        self.img = cv2.cvtColor(np.array(self.img), cv2.COLOR_BGR2RGB)

        cv2.imshow('Captured Image', self.img)
        cv2.waitKey(0)
        #cv2.destroyAllWindows()

if __name__ == '__main__':
    root = tk.Tk()
    app = ButtonImg(root)
    root.mainloop()

Solution

  • As said in the comments, the best is to use a single GUI toolkit so you need either to rewrite your code for Qt or rewrite the snipping code using tkinter. I don't know Qt much so I cannot help you with the first option. However, the screenshot is actually taken using PIL, not some Qt specific method, so the snipping code can be rewritten in tkinter.

    All you need is a fullscreen toplevel containing a canvas with a draggable rectangle, like in Drawing rectangle using mouse events in Tkinter. To make the toplevel fullscreen: toplevel.attributes('-fullscreen', True)

    The toplevel needs to be partially transparent, which can be achieved with toplevel.attributes('-alpha', <value>). I am using Linux (with XFCE desktop environment) and I need to add toplevel.attributes('-type', 'dock') to make the transparency work.

    All put together in a class, this give:

    import sys
    import tkinter as tk
    from PIL import ImageGrab
    import cv2
    import numpy as np
    
    class MyWidget(tk.Toplevel):
        def __init__(self, master):
            super().__init__(master)
            self.configure(cursor='cross')
            if sys.platform == 'linux':
                self.attributes('-type', 'dock')  # to make transparency work in Linux
            self.attributes('-fullscreen', True)
            self.attributes('-alpha', 0.3)
    
            self.canvas = tk.Canvas(self, bg='white')
            self.canvas.pack(fill='both', expand=True)
    
            self.begin_x = 0
            self.begin_y = 0
            self.end_x = 0
            self.end_y = 0
    
            self.canvas.create_rectangle(0, 0, 0, 0, outline='gray', width=3, fill='blue', tags='snip_rect')
            self.canvas.bind('<ButtonPress-1>', self.mousePressEvent)
            self.canvas.bind('<B1-Motion>', self.mouseMoveEvent)
            self.canvas.bind('<ButtonRelease-1>', self.mouseReleaseEvent)
    
            print('Capture the screen...')
    
        def mousePressEvent(self, event):
            self.begin_x = event.x
            self.begin_y = event.y
            self.end_x = self.begin_x
            self.end_y = self.begin_y
            self.canvas.coords('snip_rect', self.begin_x, self.begin_y, self.end_x, self.end_y)
    
        def mouseMoveEvent(self, event):
            self.end_x = event.x
            self.end_y = event.y
            self.canvas.coords('snip_rect', self.begin_x, self.begin_y, self.end_x, self.end_y)
    
        def mouseReleaseEvent(self, event):
            self.destroy()
            self.master.update_idletasks()
            self.master.after(100)  # give time for screen to be refreshed so as not to see the blue box on the screenshot
            x1 = min(self.begin_x, self.end_x)
            y1 = min(self.begin_y, self.end_y)
            x2 = max(self.begin_x, self.end_x)
            y2 = max(self.begin_y, self.end_y)
    
            img = ImageGrab.grab(bbox=(x1, y1, x2, y2))
            self.img = cv2.cvtColor(np.array(img), cv2.COLOR_BGR2RGB)
    
            cv2.imshow('Captured Image', self.img)
            cv2.waitKey(0)
    
    
    if __name__ == '__main__':
        root = tk.Tk()
        tk.Button(root, text='Snip', command=lambda: MyWidget(root)).pack()
        root.mainloop()