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 ...
Related
I'm fairly new to PyQt
I'm trying to drawing a line from 1 QLabel to another.
My 2 QLabel are located on another QLabel which acts as an image in my GUI.
I've managed to track the mouse event and move the label around, but I cannot draw the line between them using QPainter.
Thank you in advance :)
This is my MouseTracking class
class MouseTracker(QtCore.QObject):
positionChanged = QtCore.pyqtSignal(QtCore.QPoint)
def __init__(self, widget):
super().__init__(widget)
self._widget = widget
self.widget.setMouseTracking(True)
self.widget.installEventFilter(self)
#property
def widget(self):
return self._widget
def eventFilter(self, o, e):
if e.type() == QtCore.QEvent.MouseMove:
self.positionChanged.emit(e.pos())
return super().eventFilter(o, e)
This is my DraggableLabel class:
class DraggableLabel(QLabel):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.LabelIsMoving = False
self.setStyleSheet("border-color: rgb(238, 0, 0); border-width : 2.0px; border-style:inset; background: transparent;")
self.origin = None
# self.setDragEnabled(True)
def mousePressEvent(self, event):
if not self.origin:
# update the origin point, we'll need that later
self.origin = self.pos()
if event.button() == Qt.LeftButton:
self.LabelIsMoving = True
self.mousePos = event.pos()
# print(event.pos())
def mouseMoveEvent(self, event):
if event.buttons() == Qt.LeftButton:
# move the box
self.move(self.pos() + event.pos() - self.mousePos)
# print(event.pos())
def mouseReleaseEvent(self, event):
if event.button() == Qt.LeftButton:
print(event.pos())
def paintEvent(self, event):
painter = QPainter()
painter.setBrush(Qt.red)
# painter.setPen(qRgb(200,0,0))
painter.drawLine(10, 10, 200, 200)
This is my custom class for the QTabwigdet (since I need to control and track the position of 2 QLabels whenever the user add/insert a new Tab)
class DynamicTab(QWidget):
def __init__(self):
super(DynamicTab, self).__init__()
# self.count = 0
self.setMouseTracking(True)
self.setAcceptDrops(True)
self.bool = True
self.layout = QVBoxLayout(self)
self.label = QLabel()
self.layout.addChildWidget(self.label)
self.icon1 = DraggableLabel(parent=self)
#pixmap for icon 1
pixmap = QPixmap('icon1.png')
# currentTab.setLayout(QVBoxLayout())
# currentTab.layout.setWidget(QRadioButton())
self.icon1.setPixmap(pixmap)
self.icon1.setScaledContents(True)
self.icon1.setFixedSize(20, 20)
self.icon2 = DraggableLabel(parent=self)
pixmap = QPixmap('icon1.png')
# currentTab.setLayout(QVBoxLayout())
# currentTab.layout.setWidget(QRadioButton())
self.icon2.setPixmap(pixmap)
self.icon2.setScaledContents(True)
self.icon2.setFixedSize(20, 20)
#self.label.move(event.x() - self.label_pos.x(), event.y() - self.label_pos.y())
MainWindow and main method:
class UI_MainWindow(QMainWindow):
def __init__(self):
super(UI_MainWindow, self).__init__()
self.setWindowTitle("QHBoxLayout")
self.PictureTab = QTabWidget
def __setupUI__(self):
# super(UI_MainWindow, self).__init__()
self.setWindowTitle("QHBoxLayout")
loadUi("IIML_test2.ui", self)
self.tabChanged(self.PictureTab)
# self.tabChanged(self.tabWidget)
self.changeTabText(self.PictureTab, index=0, TabText="Patient1")
self.Button_ImportNew.clicked.connect(lambda: self.insertTab(self.PictureTab))
# self.PictureTab.currentChanged.connect(lambda: self.tabChanged(QtabWidget=self.PictureTab))
# self.tabWidget.currentChanged.connect(lambda: self.tabChanged(QtabWidget=self.tabWidget))
def tabChanged(self, QtabWidget):
QtabWidget.currentChanged.connect(lambda : print("Tab was changed to ", QtabWidget.currentIndex()))
def changeTabText(self, QTabWidget, index, TabText):
QTabWidget.setTabText(index, TabText)
def insertTab(self, QtabWidget):
# QFileDialog.getOpenFileNames(self, 'Open File', '.')
QtabWidget.addTab(DynamicTab(), "New Tab")
# get number of active tab
count = QtabWidget.count()
# change the view to the last added tab
currentTab = QtabWidget.widget(count-1)
QtabWidget.setCurrentWidget(currentTab)
pixmap = QPixmap('cat.jpg')
#currentTab.setLayout(QVBoxLayout())
#currentTab.layout.setWidget(QRadioButton())
# currentTab.setImage("cat.jpg")
currentTab.label.setPixmap(pixmap)
currentTab.label.setScaledContents(True)
currentTab.label.setFixedSize(self.label.width(), self.label.height())
tracker = MouseTracker(currentTab.label)
tracker.positionChanged.connect(self.on_positionChanged)
self.label_position = QtWidgets.QLabel(currentTab.label, alignment=QtCore.Qt.AlignCenter)
self.label_position.setStyleSheet('background-color: white; border: 1px solid black')
currentTab.label.show()
# print(currentTab.label)
#QtCore.pyqtSlot(QtCore.QPoint)
def on_positionChanged(self, pos):
delta = QtCore.QPoint(30, -15)
self.label_position.show()
self.label_position.move(pos + delta)
self.label_position.setText("(%d, %d)" % (pos.x(), pos.y()))
self.label_position.adjustSize()
# def SetupUI(self, MainWindow):
#
# self.setLayout(self.MainLayout)
if __name__ == '__main__':
app = QApplication(sys.argv)
UI_MainWindow = UI_MainWindow()
UI_MainWindow.__setupUI__()
widget = QtWidgets.QStackedWidget()
widget.addWidget(UI_MainWindow)
widget.setFixedHeight(900)
widget.setFixedWidth(1173)
widget.show()
try:
sys.exit(app.exec_())
except:
print("Exiting")
My concept: I have a DynamicTab (QTabWidget) which acts as a picture opener (whenever the user press Import Now). The child of this Widget are 3 Qlabels: self.label is the picture it self and two other Qlabels are the icon1 and icon2 which I'm trying to interact/drag with (Draggable Label)
My Problem: I'm trying to track my mouse movement and custom the painter to paint accordingly. I'm trying that out by telling the painter class to paint whenever I grab the label and move it with my mouse (Hence, draggable). However, I can only track the mouse position inside the main QLabel (the main picture) whenever I'm not holding or clicking my left mouse.
Any help will be appreciated here.
Thank you guys.
Painting can only happen within the widget rectangle, so you cannot draw outside the boundaries of DraggableLabel.
The solution is to create a further custom widget that shares the same parent, and then draw the line that connects the center of the other two.
In the following example I install an event filter on the two draggable labels which will update the size of the custom widget based on them (so that its geometry will always include those two geometries) and call self.update() which schedules a repainting. Note that since the widget is created above the other two, it might capture mouse events that are intended for the others; to prevent that, the Qt.WA_TransparentForMouseEvents attribute must be set.
class Line(QWidget):
def __init__(self, obj1, obj2, parent):
super().__init__(parent)
self.obj1 = obj1
self.obj2 = obj2
self.obj1.installEventFilter(self)
self.obj2.installEventFilter(self)
self.setAttribute(Qt.WA_TransparentForMouseEvents)
def eventFilter(self, obj, event):
if event.type() in (event.Move, event.Resize):
rect = self.obj1.geometry() | self.obj2.geometry()
corner = rect.bottomRight()
self.resize(corner.x(), corner.y())
self.update()
return super().eventFilter(obj, event)
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(painter.Antialiasing)
painter.setPen(QColor(200, 0, 0))
painter.drawLine(
self.obj1.geometry().center(),
self.obj2.geometry().center()
)
class DynamicTab(QWidget):
def __init__(self):
# ...
self.line = Line(self.icon1, self.icon2, self)
Notes:
to simplify things, I only use resize() (not setGeometry()), in this way the widget will always be placed on the top left corner of the parent and we can directly get the other widget's coordinates without any conversion;
the custom widget is placed above the other two because it is added after them; if you want to place it under them, use self.line.lower();
the painter must always be initialized with the paint device argument, either by using QPainter(obj) or painter.begin(obj), otherwise no painting will happen (and you'll get lots of errors in the output);
do not use layout.addChildWidget() (which is used internally by the layout), but the proper addWidget() function of the layout;
the stylesheet border syntax can be shortened with border: 2px inset rgb(238, 0, 0);;
the first lines of insertTab could be simpler: currentTab = DynamicTab() QtabWidget.addTab(currentTab, "New Tab");
currentTab.label.setFixedSize(self.label.size());
QMainWindow is generally intended as a top level widget, it's normally discouraged to add it to a QStackedWidget; note that if you did that because of a Youtube tutorial, that tutorial is known for suggesting terrible practices (like the final try/except block) which should not be followed;
only classes and constants should have capitalized names, not variables and functions which should always start with a lowercase letter;
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.
In my application I have a QGraphicsScene where the user should be able to change the color of items by having the mouse button clicked and hover over the items.
Below is an example code which I borrowed from another question:
PyQt: hover and click events for graphicscene ellipse
from PyQt5 import QtGui, QtCore, QtWidgets
class MyFrame(QtWidgets.QGraphicsView):
def __init__( self, parent = None ):
super(MyFrame, self).__init__(parent)
self.setScene(QtWidgets.QGraphicsScene())
# add some items
x = 0
y = 0
w = 15
h = 15
pen = QtGui.QPen(QtGui.QColor(QtCore.Qt.green))
brush = QtGui.QBrush(pen.color().darker(150))
# i want a mouse over and mouse click event for this ellipse
for xi in range(3):
for yi in range(3):
item = callbackRect(x+xi*30, y+yi*30, w, h)
item.setAcceptHoverEvents(True)
item.setPen(pen)
item.setBrush(brush)
self.scene().addItem(item)
item.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable)
class callbackRect(QtWidgets.QGraphicsRectItem):
'''
Rectangle call-back class.
'''
def mouseReleaseEvent(self, event):
# recolor on click
color = QtGui.QColor(180, 174, 185)
brush = QtGui.QBrush(color)
QtWidgets.QGraphicsRectItem.setBrush(self, brush)
return QtWidgets.QGraphicsRectItem.mouseReleaseEvent(self, event)
def hoverMoveEvent(self, event):
# Do your stuff here.
pass
def hoverEnterEvent(self, event):
color = QtGui.QColor(0, 174, 185)
brush = QtGui.QBrush(color)
QtWidgets.QGraphicsRectItem.setBrush(self, brush)
def hoverLeaveEvent(self, event):
color = QtGui.QColor(QtCore.Qt.green)
brush = QtGui.QBrush(color.darker(150))
QtWidgets.QGraphicsRectItem.setBrush(self, brush)
if ( __name__ == '__main__' ):
app = QtWidgets.QApplication([])
f = MyFrame()
f.show()
app.exec_()
So, in this code the hovering methods are only called when there is no mouse button pressed. As stated in the documentation (for PySide) the mousePressEvent "decides which graphics item it is that receives mouse events" which in some way blocks mouse events for other items.
https://deptinfo-ensip.univ-poitiers.fr/ENS/pyside-docs/PySide/QtGui/QGraphicsItem.html?highlight=graphicsitem#PySide.QtGui.PySide.QtGui.QGraphicsItem.mouseMoveEvent
However, is there a way to simultaneously hold the mouse button pressed and call hover events of different items?
The problem is the combination of events that makes the task complicated, you can propagate the mouseMoveEvent event but you can not do the same with hover events. A simple solution is to implement the logic in the method mouseMoveEvent of QGraphicsView as shown below:
class MyFrame(QtWidgets.QGraphicsView):
def __init__( self, parent = None ):
super(MyFrame, self).__init__(parent)
self.setScene(QtWidgets.QGraphicsScene())
[...]
itemsSelected = []
def mouseMoveEvent(self, event):
QtWidgets.QGraphicsView.mouseMoveEvent(self, event)
items = self.items(event.pos())#, QtGui.QTransform())
for item in self.itemsSelected:
if item in items:
item.enterColor()
else:
item.leaveColor()
self.itemsSelected = items
class callbackRect(QtWidgets.QGraphicsRectItem):
'''
Rectangle call-back class.
'''
def enterColor(self):
color = QtGui.QColor(0, 174, 185)
brush = QtGui.QBrush(color)
QtWidgets.QGraphicsRectItem.setBrush(self, brush)
def leaveColor(self):
color = QtGui.QColor(QtCore.Qt.green)
brush = QtGui.QBrush(color.darker(150))
QtWidgets.QGraphicsRectItem.setBrush(self, brush)
def hoverEnterEvent(self, event):
self.enterColor()
def hoverLeaveEvent(self, event):
self.leaveColor()
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.
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)