I am making an imitation of the built-in Win+Shift+S screenshot function on windows. I am not very familiar with QPainter. Just like the windows function, I want to darken the background, but highlight the actual selected rect the user does. Everything works, but since the background is dark the actual image is darkened. Is there a workaround for this?
import sys
from PyQt5.QtWidgets import QApplication, QWidget
from PyQt5.QtCore import Qt, QPoint, QRect, Qt
from PyQt5.QtGui import QPixmap, QPen, QPainter, QColor, QBrush
from win32api import GetSystemMetrics, GetKeyState, GetCursorPos
import pyautogui
import PIL
class MyApp(QWidget):
def __init__(self):
super().__init__()
self.setFixedSize(GetSystemMetrics(0), GetSystemMetrics(1))
self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_TranslucentBackground, True)
self.setWindowOpacity(.9)
self.setWindowFlag(Qt.Tool)
self.pix = QPixmap(self.rect().size())
(self.begin, self.destination) = (QPoint(), QPoint())
def paintEvent(self, event):
painter = QPainter(self)
painter.setOpacity(0.2)
painter.setBrush(Qt.black) #ACTUAL BACKGROUDN
painter.setPen(QPen(Qt.white)) #BORDER OF THE RECTANGLE
painter.drawRect(self.rect())
painter.drawPixmap(QPoint(), self.pix)
if not self.begin.isNull() and not self.destination.isNull():
rect = QRect(self.begin, self.destination)
painter.drawRect(rect.normalized())
def mousePressEvent(self, event):
global initial_x, initial_y
initial_x, initial_y = GetCursorPos()
print('down')
if event.buttons() & Qt.LeftButton:
self.begin = event.pos()
self.destination = self.begin
self.update()
def mouseMoveEvent(self, event):
if event.buttons() & Qt.LeftButton:
self.destination = event.pos()
self.update()
def mouseReleaseEvent(self, event):
final_x, final_y = GetCursorPos()
print('up')
a = pyautogui.screenshot(region=(initial_x,initial_y, (final_x - initial_x), (final_y - initial_y)))
a.save(r'C:\Users\ohtitus\Documents\New folder\main.png')
if event.button() & Qt.LeftButton:
rect = QRect(self.begin, self.destination)
painter = QPainter(self.pix)
painter.drawRect(rect.normalized())
painter.fillRect(rect, QColor(0,0,0,0))
(self.begin, self.destination) = (QPoint(), QPoint())
self.close()
if __name__ == '__main__':
app = QApplication(sys.argv)
app.setOverrideCursor(Qt.CrossCursor)
app.setStyleSheet("background-color: rgb(0, 0, 0)")
app.setStyleSheet('''
QWidget {
font-size: 30px;
}
''')
myApp = MyApp()
myApp.show()
try:
sys.exit(app.exec_())
except SystemExit:
pass
In your paintEvent(self, event) method, add two lines of code before drawing the transparent rectangle which specifies the region to be captured
if not self.begin.isNull() and not self.destination.isNull():
painter.setOpacity(0.0) # Added
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Source) # Added
rect = QRect(self.begin, self.destination)
painter.drawRect(rect.normalized()) # Origianl code
So paintEvent(self, event) will look like this
def paintEvent(self, event):
painter = QPainter(self)
painter.setOpacity(0.2)
painter.setBrush(Qt.black) # ACTUAL BACKGROUDN
painter.setPen(QPen(Qt.white)) # BORDER OF THE RECTANGLE
painter.drawRect(self.rect())
painter.drawPixmap(QPoint(), self.pix)
if not self.begin.isNull() and not self.destination.isNull():
painter.setOpacity(0.0)
painter.setCompositionMode(QPainter.CompositionMode.CompositionMode_Source)
rect = QRect(self.begin, self.destination)
painter.drawRect(rect.normalized())
Code explanation
painter.setOpacity(0.0) needs to draw a transparent figure
painter.setCompositionMode( mode ) changes the way how a new figure (source) will be merged into the the original drawing (destination).
By default, it is set to CompositionMode.CompositionMode_SourceOver, the mode where a source will overwrite a destination while the destiantion still appear in a transparent region of the source.
In your case, you want to make some part of the destination transparent . You can achieve this by making transparent source to overwrite the destination. The mode CompositionMode.CompositionMode_Source does it.
Refer to https://doc.qt.io/archives/qt-4.8/qpainter.html#CompositionMode-enum for more information about the composition mode.
Related
I have an application where I have a transparent window, I am capturing the screen underneath and then displaying the same once user release the left mouse button.
But the problem is I see only black screen, I tried saving the selected screenshot but still same black screen.
Here is my code :
from PyQt5 import QtWidgets as qtw
from PyQt5 import QtCore as qtc
from PyQt5 import QtGui as qtg
import sys
class MainWindow(qtw.QMainWindow):
def __init__(self, *arg, **kwargs):
super().__init__()
self.setWindowFlag(qtc.Qt.FramelessWindowHint)
self.setAttribute(qtc.Qt.WA_TranslucentBackground)
borderWidget = qtw.QWidget(objectName='borderWidget')
self.setCentralWidget(borderWidget)
bgd = self.palette().color(qtg.QPalette.Window)
bgd.setAlphaF(.005)
self.setStyleSheet('''
#borderWidget {{
border: 3px solid blue;
background: {bgd};
}}
'''.format(bgd=bgd.name(bgd.HexArgb)))
self.setGeometry(100, 100, 400, 300)
self.showFullScreen()
self.setCursor(qtc.Qt.CrossCursor)
self.begin = None
self.end = None
self.show()
def paintEvent(self, event):
if self.begin:
qpbox = qtg.QPainter(self)
br = qtg.QBrush(qtg.QColor(100, 10, 10, 40))
qpbox.setBrush(br)
qpbox.drawRect(qtc.QRect(self.begin, self.end))
# close on right click
def mouseReleaseEvent(self, QMouseEvent):
if QMouseEvent.button() == qtc.Qt.RightButton:
self.close()
elif QMouseEvent.button() == qtc.Qt.LeftButton:
screen = qtw.QApplication.primaryScreen()
img = screen.grabWindow(self.winId(), self.begin.x(), self.end.y(), self.end.x() - self.begin.x() , self.end.y()-self.begin.y())
img.save('screenshot.png', 'png')
self.setStyleSheet("")
self.central_widget = qtw.QWidget()
label = qtw.QLabel(self)
label.setPixmap(img)
self.resize(img.width(), img.height())
self.setCentralWidget(label)
def mousePressEvent(self, QMouseEvent):
if QMouseEvent.button() == qtc.Qt.LeftButton:
self.begin = QMouseEvent.pos()
self.end = QMouseEvent.pos()
self.update()
def mouseMoveEvent(self, QMouseEvent):
self.end = QMouseEvent.pos()
self.update()
if __name__ == '__main__':
app = qtw.QApplication(sys.argv)
w = MainWindow()
sys.exit(app.exec_())
You're grabbing from the current window, not from the desktop. While what you see is the desktop (due to the transparency), specifying a window id results in grabbing only that window without considering the background composition or any other foreign window.
If you want to grab from the screen, you need to use the root window's id, which is 0.
Also note that:
the coordinates are wrong, as you used self.end for the y coordinate;
if the user selects a negative rectangle, the result is unexpected; you should use a normalized rectangle instead (which always have positive width and height);
you should hide the widget before properly taking the screenshot, otherwise you will also capture the darkened background of the capture area;
there's no need to always replace the central widget, just use an empty QLabel and change its pixmap;
an alpha value of 0.005 is practically pointless, just make it transparent;
the capture rectangle should be cleared after the screenshot has been taken;
class MainWindow(qtw.QMainWindow):
def __init__(self, *arg, **kwargs):
super().__init__()
self.setWindowFlags(self.windowFlags() | qtc.Qt.FramelessWindowHint)
self.setAttribute(qtc.Qt.WA_TranslucentBackground)
# use a QLabel
borderWidget = qtw.QLabel(objectName='borderWidget')
self.setCentralWidget(borderWidget)
self.setStyleSheet('''
#borderWidget {{
border: 3px solid blue;
background: transparent;
}}
''')
# pointless, you're showing the window in full screen
# self.setGeometry(100, 100, 400, 300)
# variables that are required for painting must be declared *before*
# calling any show* function; while this is generally not an issue,
# as painting will actually happen "later", it's conceptually wrong
# to declare a variable after it's (possibly) required by a function.
self.captureRect = None
self.showFullScreen()
self.setCursor(qtc.Qt.CrossCursor)
# unnecessary, you've already called showFullScreen
# self.show()
def paintEvent(self, event):
if self.captureRect:
qpbox = qtg.QPainter(self)
br = qtg.QBrush(qtg.QColor(100, 10, 10, 40))
qpbox.setBrush(br)
qpbox.drawRect(self.captureRect)
def mouseReleaseEvent(self, event):
if event.button() == qtc.Qt.RightButton:
self.close()
elif event.button() == qtc.Qt.LeftButton:
self.hide()
screen = qtw.QApplication.primaryScreen()
img = screen.grabWindow(0, *self.captureRect.getRect())
self.show()
img.save('screenshot.png', 'png')
self.setStyleSheet('')
self.centralWidget().setPixmap(img)
self.captureRect = None
def mousePressEvent(self, event):
if event.button() == qtc.Qt.LeftButton:
self.begin = event.pos()
self.captureRect = qtc.QRect(self.begin, qtc.QSize())
def mouseMoveEvent(self, event):
self.captureRect = qtc.QRect(self.begin, event.pos()).normalized()
self.update()
Note that I changed the event handler argument: QMouseEvent is a class, and even though you're using the module (so the actual Qt class would be qtg.QMouseEvent), that might be confusing and risky if you eventually decide to directly import classes; besides, only class and constant names should have capitalized names, not variables or functions.
I am trying to draw over image using QPainter. It works good when using solid color. When using semi transparent color, dots were appearing.
Also when drawing multiple lines in one place, the color gets multiplied and produces a darker color.
import sys
from PyQt5.QtCore import Qt, QPoint
from PyQt5.QtWidgets import QMainWindow, QApplication
from PyQt5.QtGui import QPixmap, QPainter, QPen, QColor
class Menu(QMainWindow):
def __init__(self):
super().__init__()
self.drawing = False
self.lastPoint = QPoint()
self.image = QPixmap(r"C:\Users\www\Desktop\image.jpg")
self.setGeometry(100, 100, 500, 300)
self.resize(self.image.width(), self.image.height())
self.show()
def paintEvent(self, event):
painter = QPainter(self)
painter.drawPixmap(self.rect(), self.image)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.drawing = True
self.lastPoint = event.pos()
def mouseMoveEvent(self, event):
if event.buttons() and Qt.LeftButton and self.drawing:
painter = QPainter(self.image)
painter.setPen(QPen(QColor(121,252,50,50), 20, Qt.SolidLine))
painter.drawLine(self.lastPoint, event.pos())
self.lastPoint = event.pos()
self.update()
def mouseReleaseEvent(self, event):
if event.button == Qt.LeftButton:
self.drawing = False
if __name__ == '__main__':
app = QApplication(sys.argv)
mainMenu = Menu()
sys.exit(app.exec_())
I need to keep the color as the original color(instead of getting darker each time) when drawing several times over the same place.
Mouse movement are "discrete", which means that whenever you move your mouse you won't get continuous pixel coordinates: if you move your mouse fast enough from (0, 0) to (20, 20), you'll probably get only two or three mouseMoveEvents in the middle at most, resulting in single segments for each mouse event.
The "dots" you see are actually areas where the different lines you draw collide, expecially since the mouse movement are not continuous. If you think of it as painting with watercolors, it's like if you draw a small line at each mouse movement, then wait until it's dried, then start to paint another line from the previous point.
As soon as you draw unique lines at each mouseMoveEvent, the edges of those segments are superimposed, resulting in those "less transparent dots" (since you're using a non opaque color), which are the points where the segments collide, and, because painting is usually "additive" you get two or more areas where the superimposed color results in a more opaque one: imagine it as watching through two pairs of sunglasses that are not aligned.
QPainterPath, instead, can draw continuous lines without that "artifact", as long as they are part of the same painter path (no matter its subpath, including subpath polygons, ellipses, arcs, etc.). Then, whenever you tell the QPainter to draw a new element, it will be superimposed to the previous ones.
To better clarify, in this image on the left I'm drawing two distinct lines with a common vertex, using your color, which would be the case of a mousePressEvent (start drawing), a fast movement to the right (draw the first line) and another one to the bottom (draw another line). On the right there are the same "lines", but using a unique QPainterPath.
In this example code I temporarily create a painter path, which stores the current "drawing path" until the mouse is released, after which the path is actually applied to the QPixmap.
import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QWidget, QApplication
from PyQt5.QtGui import QPixmap, QPainter, QPen, QColor, QPainterPath
class Menu(QWidget):
def __init__(self):
super().__init__()
self.drawingPath = None
self.image = QPixmap(r"testimage.jpg")
self.resize(self.image.width(), self.image.height())
self.show()
def paintEvent(self, event):
painter = QPainter(self)
painter.drawPixmap(self.rect(), self.image)
if self.drawingPath:
painter.setPen(QPen(QColor(121,252,50,50), 20, Qt.SolidLine))
painter.drawPath(self.drawingPath)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
# start a new QPainterPath and *move* to the current point
self.drawingPath = QPainterPath()
self.drawingPath.moveTo(event.pos())
def mouseMoveEvent(self, event):
if event.buttons() and Qt.LeftButton and self.drawingPath:
# add a line to the painter path, without "removing" the pen
self.drawingPath.lineTo(event.pos())
self.update()
def mouseReleaseEvent(self, event):
if event.button() == Qt.LeftButton and self.drawingPath:
# draw the painter path to the pixmap
painter = QPainter(self.image)
painter.setPen(QPen(QColor(121,252,50,50), 20, Qt.SolidLine))
painter.drawPath(self.drawingPath)
self.drawingPath = None
self.update()
if __name__ == '__main__':
app = QApplication(sys.argv)
mainMenu = Menu()
sys.exit(app.exec_())
There is only one problem with this: drawing over a currently drawing path won't result in a more opaque color, meaning that, as long as the mouse button is pressed, no matter how many times you "paint" over the same point, the color will always be the same. To get the "more opaque color" effect, you'll need to paint over the intersection(s), starting a new path each time.
PS: I used a QWidget, as in some cases a QMainWindow can grab mouse movements starting from a click on a non interactive area (like in this case) and use it to move the interface.
I'm working on an opensource markdown supported minimal note taking application for Windows/Linux. I'm trying to remove the title bar and add my own buttons. I want something like, a title bar with only two custom buttons as shown in the figure
Currently I have this:
I've tried modifying the window flags:
With not window flags, the window is both re-sizable and movable. But no custom buttons.
Using self.setWindowFlags(QtCore.Qt.FramelessWindowHint), the window has no borders, but cant move or resize the window
Using self.setWindowFlags(QtCore.Qt.CustomizeWindowHint), the window is resizable but cannot move and also cant get rid of the white part at the top of the window.
Any help appreciated. You can find the project on GitHub here.
Thanks..
This is my python code:
from PyQt5 import QtCore, QtWidgets, QtWebEngineWidgets, uic
import sys
import os
import markdown2 # https://github.com/trentm/python-markdown2
from PyQt5.QtCore import QRect
from PyQt5.QtGui import QFont
simpleUiForm = uic.loadUiType("Simple.ui")[0]
class SimpleWindow(QtWidgets.QMainWindow, simpleUiForm):
def __init__(self, parent=None):
QtWidgets.QMainWindow.__init__(self, parent)
self.setupUi(self)
self.markdown = markdown2.Markdown()
self.css = open(os.path.join("css", "default.css")).read()
self.editNote.setPlainText("")
#self.noteView = QtWebEngineWidgets.QWebEngineView(self)
self.installEventFilter(self)
self.displayNote.setContextMenuPolicy(QtCore.Qt.NoContextMenu)
#self.setWindowFlags(QtCore.Qt.FramelessWindowHint)
def eventFilter(self, object, event):
if event.type() == QtCore.QEvent.WindowActivate:
print("widget window has gained focus")
self.editNote.show()
self.displayNote.hide()
elif event.type() == QtCore.QEvent.WindowDeactivate:
print("widget window has lost focus")
note = self.editNote.toPlainText()
htmlNote = self.getStyledPage(note)
# print(note)
self.editNote.hide()
self.displayNote.show()
# print(htmlNote)
self.displayNote.setHtml(htmlNote)
elif event.type() == QtCore.QEvent.FocusIn:
print("widget has gained keyboard focus")
elif event.type() == QtCore.QEvent.FocusOut:
print("widget has lost keyboard focus")
return False
The UI file is created in the following hierarchy
Here are the steps you just gotta follow:
Have your MainWindow, be it a QMainWindow, or QWidget, or whatever [widget] you want to inherit.
Set its flag, self.setWindowFlags(Qt.FramelessWindowHint)
Implement your own moving around.
Implement your own buttons (close, max, min)
Implement your own resize.
Here is a small example with move around, and buttons implemented. You should still have to implement the resize using the same logic.
import sys
from PyQt5.QtCore import QPoint
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication
from PyQt5.QtWidgets import QHBoxLayout
from PyQt5.QtWidgets import QLabel
from PyQt5.QtWidgets import QPushButton
from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QWidget
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.layout = QVBoxLayout()
self.layout.addWidget(MyBar(self))
self.setLayout(self.layout)
self.layout.setContentsMargins(0,0,0,0)
self.layout.addStretch(-1)
self.setMinimumSize(800,400)
self.setWindowFlags(Qt.FramelessWindowHint)
self.pressing = False
class MyBar(QWidget):
def __init__(self, parent):
super(MyBar, self).__init__()
self.parent = parent
print(self.parent.width())
self.layout = QHBoxLayout()
self.layout.setContentsMargins(0,0,0,0)
self.title = QLabel("My Own Bar")
btn_size = 35
self.btn_close = QPushButton("x")
self.btn_close.clicked.connect(self.btn_close_clicked)
self.btn_close.setFixedSize(btn_size,btn_size)
self.btn_close.setStyleSheet("background-color: red;")
self.btn_min = QPushButton("-")
self.btn_min.clicked.connect(self.btn_min_clicked)
self.btn_min.setFixedSize(btn_size, btn_size)
self.btn_min.setStyleSheet("background-color: gray;")
self.btn_max = QPushButton("+")
self.btn_max.clicked.connect(self.btn_max_clicked)
self.btn_max.setFixedSize(btn_size, btn_size)
self.btn_max.setStyleSheet("background-color: gray;")
self.title.setFixedHeight(35)
self.title.setAlignment(Qt.AlignCenter)
self.layout.addWidget(self.title)
self.layout.addWidget(self.btn_min)
self.layout.addWidget(self.btn_max)
self.layout.addWidget(self.btn_close)
self.title.setStyleSheet("""
background-color: black;
color: white;
""")
self.setLayout(self.layout)
self.start = QPoint(0, 0)
self.pressing = False
def resizeEvent(self, QResizeEvent):
super(MyBar, self).resizeEvent(QResizeEvent)
self.title.setFixedWidth(self.parent.width())
def mousePressEvent(self, event):
self.start = self.mapToGlobal(event.pos())
self.pressing = True
def mouseMoveEvent(self, event):
if self.pressing:
self.end = self.mapToGlobal(event.pos())
self.movement = self.end-self.start
self.parent.setGeometry(self.mapToGlobal(self.movement).x(),
self.mapToGlobal(self.movement).y(),
self.parent.width(),
self.parent.height())
self.start = self.end
def mouseReleaseEvent(self, QMouseEvent):
self.pressing = False
def btn_close_clicked(self):
self.parent.close()
def btn_max_clicked(self):
self.parent.showMaximized()
def btn_min_clicked(self):
self.parent.showMinimized()
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
mw.show()
sys.exit(app.exec_())
Here are some tips:
Option 1:
Have a QGridLayout with widget in each corner and side(e.g. left, top-left, menubar, top-right, right, bottom-right, bottom and bottom left)
With the approach (1) you would know when you are clicking in each border, you just got to define each one size and add each one on their place.
When you click on each one treat them in their respective ways, for example, if you click in the left one and drag to the left, you gotta resize it larger and at the same time move it to the left so it will appear to be stopped at the right place and grow width.
Apply this reasoning to each edge, each one behaving in the way it has to.
Option 2:
Instead of having a QGridLayout you can detect in which place you are clicking by the click pos.
Verify if the x of the click is smaller than the x of the moving pos to know if it's moving left or right and where it's being clicked.
The calculation is made in the same way of the Option1
Option 3:
Probably there are other ways, but those are the ones I just thought of. For example using the CustomizeWindowHint you said you are able to resize, so you just would have to implement what I gave you as example. BEAUTIFUL!
Tips:
Be careful with the localPos(inside own widget), globalPos(related to your screen). For example: If you click in the very left of your left widget its 'x' will be zero, if you click in the very left of the middle(content)it will be also zero, although if you mapToGlobal you will having different values according to the pos of the screen.
Pay attention when resizing, or moving, when you have to add width or subtract, or just move, or both, I'd recommend you to draw on a paper and figure out how the logic of resizing works before implementing it out of blue.
GOOD LUCK :D
While the accepted answer can be considered valid, it has some issues.
using setGeometry() is not appropriate (and the reason for using it was wrong) since it doesn't consider possible frame margins set by the style;
the position computation is unnecessarily complex;
resizing the title bar to the total width is wrong, since it doesn't consider the buttons and can also cause recursion problems in certain situations (like not setting the minimum size of the main window); also, if the title is too big, it makes impossible to resize the main window;
buttons should not accept focus;
setting a layout creates a restraint for the "main widget" or layout, so the title should not be added, but the contents margins of the widget should be used instead;
I revised the code to provide a better base for the main window, simplify the moving code, and add other features like the Qt windowTitle() property support, standard QStyle icons for buttons (instead of text), and proper maximize/normal button icons. Note that the title label is not added to the layout.
class MainWindow(QWidget):
def __init__(self):
super(MainWindow, self).__init__()
self.setWindowFlags(self.windowFlags() | Qt.FramelessWindowHint)
self.titleBar = MyBar(self)
self.setContentsMargins(0, self.titleBar.height(), 0, 0)
self.resize(640, self.titleBar.height() + 480)
def changeEvent(self, event):
if event.type() == event.WindowStateChange:
self.titleBar.windowStateChanged(self.windowState())
def resizeEvent(self, event):
self.titleBar.resize(self.width(), self.titleBar.height())
class MyBar(QWidget):
clickPos = None
def __init__(self, parent):
super(MyBar, self).__init__(parent)
self.setAutoFillBackground(True)
self.setBackgroundRole(QPalette.Shadow)
# alternatively:
# palette = self.palette()
# palette.setColor(palette.Window, Qt.black)
# palette.setColor(palette.WindowText, Qt.white)
# self.setPalette(palette)
layout = QHBoxLayout(self)
layout.setContentsMargins(1, 1, 1, 1)
layout.addStretch()
self.title = QLabel("My Own Bar", self, alignment=Qt.AlignCenter)
# if setPalette() was used above, this is not required
self.title.setForegroundRole(QPalette.Light)
style = self.style()
ref_size = self.fontMetrics().height()
ref_size += style.pixelMetric(style.PM_ButtonMargin) * 2
self.setMaximumHeight(ref_size + 2)
btn_size = QSize(ref_size, ref_size)
for target in ('min', 'normal', 'max', 'close'):
btn = QToolButton(self, focusPolicy=Qt.NoFocus)
layout.addWidget(btn)
btn.setFixedSize(btn_size)
iconType = getattr(style,
'SP_TitleBar{}Button'.format(target.capitalize()))
btn.setIcon(style.standardIcon(iconType))
if target == 'close':
colorNormal = 'red'
colorHover = 'orangered'
else:
colorNormal = 'palette(mid)'
colorHover = 'palette(light)'
btn.setStyleSheet('''
QToolButton {{
background-color: {};
}}
QToolButton:hover {{
background-color: {}
}}
'''.format(colorNormal, colorHover))
signal = getattr(self, target + 'Clicked')
btn.clicked.connect(signal)
setattr(self, target + 'Button', btn)
self.normalButton.hide()
self.updateTitle(parent.windowTitle())
parent.windowTitleChanged.connect(self.updateTitle)
def updateTitle(self, title=None):
if title is None:
title = self.window().windowTitle()
width = self.title.width()
width -= self.style().pixelMetric(QStyle.PM_LayoutHorizontalSpacing) * 2
self.title.setText(self.fontMetrics().elidedText(
title, Qt.ElideRight, width))
def windowStateChanged(self, state):
self.normalButton.setVisible(state == Qt.WindowMaximized)
self.maxButton.setVisible(state != Qt.WindowMaximized)
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
self.clickPos = event.windowPos().toPoint()
def mouseMoveEvent(self, event):
if self.clickPos is not None:
self.window().move(event.globalPos() - self.clickPos)
def mouseReleaseEvent(self, QMouseEvent):
self.clickPos = None
def closeClicked(self):
self.window().close()
def maxClicked(self):
self.window().showMaximized()
def normalClicked(self):
self.window().showNormal()
def minClicked(self):
self.window().showMinimized()
def resizeEvent(self, event):
self.title.resize(self.minButton.x(), self.height())
self.updateTitle()
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
layout = QVBoxLayout(mw)
widget = QTextEdit()
layout.addWidget(widget)
mw.show()
mw.setWindowTitle('My custom window with a very, very long title')
sys.exit(app.exec_())
This is for the people who are going to implement custom title bar in PyQt6 or PySide6
The below changes should be done in the answer given by #musicamante
def mousePressEvent(self, event):
if event.button() == Qt.LeftButton:
# self.clickPos = event.windowPos().toPoint()
self.clickPos = event.scenePosition().toPoint()
def mouseMoveEvent(self, event):
if self.clickPos is not None:
# self.window().move(event.globalPos() - self.clickPos)
self.window().move(event.globalPosition().toPoint() - self.clickPos)
if __name__ == "__main__":
app = QApplication(sys.argv)
mw = MainWindow()
mw.show()
# sys.exit(app.exec_())
sys.exit(app.exec())
References:
QMouseEvent.globalPosition(),
QMouseEvent.scenePosition()
This method of moving Windows with Custom Widget doesn't work with WAYLAND. If anybody has a solution for that please post it here for future reference
Working functions for WAYLAND and PyQT6/PySide6 :
def mousePressEvent(self, event):
if event.button() == Qt.MouseButton.LeftButton:
self._move()
return super().mousePressEvent(event)
def _move(self):
window = self.window().windowHandle()
window.startSystemMove()
Please check.
Here is some code that illustrates my problem:
import sys
from PyQt4 import QtGui, QtCore
class CustomButton(QtGui.QAbstractButton):
def __init__(self, *__args):
super().__init__(*__args)
self.setFixedSize(190, 50)
self.installEventFilter(self)
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.setBrush(QtGui.QColor(136, 212, 78))
painter.setPen(QtCore.Qt.NoPen)
painter.drawRect(QtCore.QRect(0, 0, 100, 48))
def eventFilter(self, object, event):
if event.type() == QtCore.QEvent.HoverMove:
painter = QtGui.QPainter(self)
painter.begin(self)
painter.drawRect(QtCore.QRect(0, 0, 100, 48))
painter.end()
return True
return False
app = QtGui.QApplication(sys.argv)
window = QtGui.QWidget()
layout = QtGui.QGridLayout(window)
button = CustomButton()
layout.addWidget(button, 0, 0)
window.show()
sys.exit(app.exec_())
The goal is to make a button using QPainter that can be modified when the HoverMove event is detected. However, I get the following errors upon hovering:
QPainter::begin: Paint device returned engine == 0, type: 1
QPainter::begin: Paint device returned engine == 0, type: 1
QPainter::drawRects: Painter not active
QPainter::end: Painter not active, aborted
From what I've understood from the docs (here) I can use .begin() to activate the QPainter; however as the error message shows this isn't the case and the second rectangle does not get drawn. How should I use QPainter to achieve the desired output ?
You need to detect the hover inside paintEvent and act accordingly:
def paintEvent(self, event):
option = QtGui.QStyleOptionButton()
option.initFrom(self)
painter = QtGui.QPainter(self)
if option.state & QtGui.QStyle.State_MouseOver:
# do hover stuff ...
else:
# do normal stuff ...
QStyleOption and its subclasses contain all the information that QStyle functions need to draw a graphical element. The QPaintEvent only contains information about what area needs to be updated.
#ekhumoro's answer above can be rewritten more simply with QWidget::underMouse(). the docs
def paintEvent(self, event):
if underMouse():
# do hover stuff ...
else:
# do normal stuff ...
I have a QMainWindow which contains a DrawingPointsWidget. This widget draws red points randomly. I display the mouse coordinates in the QMainWindow's status bar by installing an event filter for the MouseHovering event using self.installEventFilter(self) and by implementing the eventFilter() method . It works. However I want to get the mouse coordinates on this red-points widget, and not on the QMainWindow. So I want the status bar to display [0, 0] when the mouse is at the top-left corner of the points widget, and not of the QMainWindow. How do I do that? I tried self.installEventFilter(points) but nothing happens.
You wil find below a working chunck of code.
EDIT 1
It seems that if I write points.installEventFilter(self), the QtCore.Event.MouseButtonPressed event is detected, only the HoverMove is not. So the HoverMove event is not detected on my DrawingPointsWidget which is a QWidget.
Surprisingly, the HoverMove event is detected on the QPushButton which is a QAbstractButton which is a QWidget too! I need to write button.installEventFilter(self)
import sys
import random
from PyQt4 import QtGui, QtCore
from PyQt4.QtGui import *
class MainWindow(QMainWindow):
def __init__(self):
QtGui.QMainWindow.__init__(self)
self.__setUI()
def __setUI(self, appTitle="[default title]"):
self.statusBar()
mainWidget = QWidget()
vbox = QVBoxLayout()
button = QPushButton("Hello")
vbox.addWidget( button )
points = DrawingPointsWidget()
vbox.addWidget(points)
mainWidget.setLayout(vbox)
self.setCentralWidget(mainWidget)
self.installEventFilter(self)
def eventFilter(self, object, event):
if event.type() == QtCore.QEvent.HoverMove:
mousePosition = event.pos()
cursor = QtGui.QCursor()
self.statusBar().showMessage(
"Mouse: [" + mousePosition.x().__str__() + ", " + mousePosition.y().__str__() + "]"
+ "\tCursor: [" + cursor.pos().x().__str__() + ", " + cursor.pos().y().__str__() + "]"
)
return True
elif event.type() == QtCore.QEvent.MouseButtonPress:
print "Mouse pressed"
return True
return False
class DrawingPointsWidget(QWidget):
""
def __init__(self):
super(QWidget, self).__init__()
self.__setUI()
def __setUI(self):
self.setGeometry(300, 300, 280, 170)
self.setWindowTitle('Points')
self.show()
def paintEvent(self, e):
"Re-implemented method"
qp = QtGui.QPainter()
qp.begin(self)
self.drawPoints(qp)
qp.end()
def drawPoints(self, qp):
qp.setPen(QtCore.Qt.red)
"Need to get the size in case the window is resized -> generates a new paint event"
size = self.size()
for i in range(1000):
x = random.randint(1, size.width()-1 )
y = random.randint(1, size.height()-1 )
qp.drawPoint(x, y)
def main():
app = QApplication(sys.argv)
#window = WidgetsWindow2()
window = MainWindow()
window.show()
sys.exit(app.exec_())
if __name__ == "__main__":
main()
Firstly, the event filter needs to be set by the object you want to watch:
points.installEventFilter(self)
Secondly, the event you need to listen for is MouseMove not HoverMove:
if event.type() == QtCore.QEvent.MouseMove:
Finally, you need to enable mouse-tracking on the target widget:
class DrawingPointsWidget(QWidget):
def __init__(self):
super(QWidget, self).__init__()
self.setMouseTracking(True)