0

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()
Wes Tomer
  • 319
  • 1
  • 13
  • The purpose of this program is to change the image of the `button` to either a selected image, or a screen-snip. I simplified this code, but it's part of my larger program for creating instruction data with images & text. – Wes Tomer Aug 04 '20 at 19:55
  • Also, for anyone who attempts to solve/ answer this: I'm curious about your usual method to debug problems, especially when you aren't getting any error messages. – Wes Tomer Aug 04 '20 at 19:58
  • 1
    Mixing frameworks is rarely a good idea. Why do you need both Tk *and* Qt? Can't you use just one of them? – musicamante Aug 04 '20 at 20:00
  • 1
    Why is it necessary to use tkinter? you shouldn't combine those libraries – eyllanesc Aug 04 '20 at 20:08
  • I'm new to Tk & Qt, and after coding most of my work in Tk, I searched for a solution for screen-capture, and found one on GitHub that I adapted for my program. I guess I need to re-write the entire thing in Qt. Bummer. – Wes Tomer Aug 04 '20 at 23:05
  • You should have analyzed your requirements and dependencies _before_ coding… However, if the “simplified code” is basically everything you have done so far, it wouldn’t require an unachievable amount of work to rewrite it. – Melebius Aug 05 '20 at 11:45

1 Answers1

2

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()
j_4321
  • 15,431
  • 3
  • 34
  • 61