I tried to use setToolTip of QGroupBox, but the tooltip shows in all places in the group box. What I want is to only show the tooltip in the title label.
Is is even possible? If not, why the QGroupBox is designed that way?
It is possible to control the tool-tip behaviour, but there's no built-in method to do that, so you just need to add a little custom event-handling yourself. Here's a basic demo that implements that using an event-filter:
from PyQt5 import QtCore, QtGui, QtWidgets
class Window(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.button = QtWidgets.QPushButton('Test')
self.button.setToolTip('Button ToolTip')
self.group = QtWidgets.QGroupBox('Title')
self.group.installEventFilter(self)
self.group.setToolTip('Groupbox ToolTip')
self.group.setCheckable(True)
hbox = QtWidgets.QHBoxLayout(self.group)
hbox.addWidget(self.button)
layout = QtWidgets.QVBoxLayout(self)
layout.addWidget(self.group)
def eventFilter(self, source, event):
if (event.type() == QtCore.QEvent.ToolTip and
isinstance(source, QtWidgets.QGroupBox)):
options = QtWidgets.QStyleOptionGroupBox()
source.initStyleOption(options)
control = source.style().hitTestComplexControl(
QtWidgets.QStyle.CC_GroupBox, options, event.pos())
if (control != QtWidgets.QStyle.SC_GroupBoxLabel and
control != QtWidgets.QStyle.SC_GroupBoxCheckBox):
QtWidgets.QToolTip.hideText()
return True
return super().eventFilter(source, event)
if __name__ == '__main__':
app = QtWidgets.QApplication(['Test'])
window = Window()
window.setGeometry(600, 100, 300, 200)
window.show()
app.exec_()
An alternative solution would be to create a subclass and override the event method directly:
class GroupBox(QtWidgets.QGroupBox):
def event(self, event):
if event.type() == QtCore.QEvent.ToolTip:
options = QtWidgets.QStyleOptionGroupBox()
self.initStyleOption(options)
control = self.style().hitTestComplexControl(
QtWidgets.QStyle.CC_GroupBox, options, event.pos())
if (control != QtWidgets.QStyle.SC_GroupBoxLabel and
control != QtWidgets.QStyle.SC_GroupBoxCheckBox):
QtWidgets.QToolTip.hideText()
return True
return super().event(event)
QWidget::setToolTip, by defaults, set the tooltip for the whole widget. QGroupBox does not provide a special tooltip behavior, so the tooltip is visible for the whole group box, including its children (except if those provide their own tooltips). However, you are ably to customise this behaviour yourself, by reimplementing QWidget::event:
If you want to control a tooltip's behavior, you can intercept the
event() function and catch the QEvent::ToolTip event (e.g., if you
want to customize the area for which the tooltip should be shown).
Two approaches are possible:
Capture the QEvent::ToolTip event on the QGroupBox itself. See the comment of musicamante on how to determine the title region.
Capture the QEvent::ToolTip on the children of the QGroupBox and suppress the the event.
A full blown custom tooltip example is given in the Qt documentation itself.
Related
A Qt packing layout, such as QVBoxLayout, can pack widgets inside it, such as buttons. In this case, they will be packed vertically as shown in image below:
When we pack too many widgets inside such a layout, and since scrolling is not added by default, the buttons will eventually get squeezed onto each other up to a point that they will overlap, as shown below:
My questions are:
How to tell Qt to not show/pack widgets beyond the available viewing space in the non-scrolling layout?
How to handle the case when the window is resized? I.e. Qt should add/remove widgets accordingly. E.g. if there is extra space available, then perhaps Qt should add some extra widgets that it couldn't add previously.
To be specific: "too many packed widgets" is when the widgets start invading spaces of other widgets, including their inter-widget spacings or margins.
Appendix
Images above are generated by this code below as run in a tile in i3, which is a modified version of this.
from PyQt5 import QtCore, QtWidgets
app = QtWidgets.QApplication([])
widget = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout(widget)
for i in range(40):
layout.addWidget(QtWidgets.QPushButton(str(i + 1)))
widget.show()
app.exec_()
When too many widgets are packed:
If the window is tiled, you see them overcrowded as in in the image.
If the window is floating, the window will keep growing until it is no longer fully visible in the monitor.
None of these outcomes are acceptable in my case. My goal is to have Qt only pack as much as will be visible, and add/remove/hide/show dynamically as the window gets resized.
Try this code. It does not rely on QVBoxLayout but it basically does the same as this layout. It hides the child widgets which are outside of the area. There are no partially visible widgets.
from PyQt5 import QtWidgets
class Container(QtWidgets.QWidget):
_spacing = 5
def __init__(self, parent=None):
super().__init__(parent)
y = self._spacing
for i in range(40):
button = QtWidgets.QPushButton("Button" + str(i + 1), self)
button.move(self._spacing, y)
y += button.sizeHint().height() + self._spacing
def resizeEvent(self, event):
super().resizeEvent(event)
for child in self.children():
if isinstance(child, QtWidgets.QWidget):
child.resize(self.width() - 2 * self._spacing, child.height())
child.setVisible(child.geometry().bottom() < self.height())
app = QtWidgets.QApplication([])
w = Container()
w.resize(500, 500)
w.show()
app.exec_()
Note that is in fact does not add nor remove widgets dynamically, this would be much more code and it would probably be very depending on your specific use case. Moreover it feels as a premature optimization. Unless you really need it, do not do it.
UPDATE:
I experimented with the code above and proposed some improvements. I especially wanted to make it responsive to changes in child widgets. The problem is that if the child widget changes it size, the parent container must be re-layouted. The code above does not react in any way. To make it responsive, we need to react to LayoutRequest event. Note that in the code below, I have created three types of buttons - one add a line to itself, other increases font size, and yet another decreases font size.
from PyQt5 import QtCore, QtWidgets
def changeFontSize(increment):
font = QtWidgets.QApplication.font()
font.setPointSize(font.pointSize() + increment)
QtWidgets.QApplication.setFont(font)
class Container(QtWidgets.QWidget):
_spacing = 5
_children = [] # maintains the order of creation unlike children()
def __init__(self, parent=None):
super().__init__(parent)
for i in range(100):
child = QtWidgets.QPushButton(self)
child.installEventFilter(self)
# these are just to test various changes in child widget itself to force relayout
r = i % 3
if r == 0:
text = "New line"
onClicked = lambda state, w=child: w.setText(w.text() + "\nclicked")
elif r == 1:
text = "Bigger font"
onClicked = lambda: changeFontSize(1)
elif r == 2:
text = "Smaller font"
onClicked = lambda: changeFontSize(-1)
child.setText(text)
child.clicked.connect(onClicked)
self._children.append(child)
def resizeEvent(self, event):
super().resizeEvent(event)
self._relayout()
def event(self, event):
if event.type() == QtCore.QEvent.LayoutRequest:
self._relayout()
return super().event(event)
def _relayout(self):
y = self._spacing
for child in self._children:
h = child.sizeHint().height()
child.move(self._spacing, y)
child.resize(self.width() - 2 * self._spacing, h)
y += h + self._spacing
child.setVisible(y < self.height())
app = QtWidgets.QApplication([])
w = Container()
w.resize(500, 500)
w.show()
app.exec_()
This code is satisfactory, however it is not perfect. I have observed that when the container is being re-layouted and some of the child widgets will change its visibility state, re-layouting is called again. This is not needed but I have not discovered how to prevent it.
Maybe there is some better way...
I'm having a lot of problems getting the details right for my QAbstractScrollArea. This is my current implementation of viewportEvent:
def viewportEvent(self, event):
if event.type() in [QEvent.MouseButtonPress,
QEvent.MouseMove,
QEvent.MouseButtonRelease,
QEvent.ContextMenu,
QEvent.KeyPress,
QEvent.KeyRelease]:
return self.my_viewport.event(event)
if event.type() == QEvent.Resize:
self.my_viewport.resizeEvent(event)
return super().viewportEvent(event)
if event.type() in [QEvent.UpdateLater,
QEvent.UpdateRequest]:
self.my_viewport.event(event)
if event.type() == QEvent.Paint:
self.my_viewport.paintEvent(event)
return super().viewportEvent(event)
The idea is to pass through (to the viewport widget) things like key and mouse presses. Resize events need to be passed through and sent to the abstract-scroll-area itself? What about the size for the scroll bars? Shouldn't the resize event's size be changed. If I don't pass paint events through, the viewport widget doesn't paint.
Minimum working example of broken QOpenGLWidget with QAbstractScrollArea:
import sys
from PyQt5.QtCore import QEvent
from PyQt5.QtWidgets import (QAbstractScrollArea, QApplication, QMainWindow,
QOpenGLWidget)
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.scope_view_widget = ScrollingScopeView()
self.setCentralWidget(self.scope_view_widget)
class ScopeView(QOpenGLWidget):
def paintGL(self):
super().paintGL()
print("Painting")
class ScrollingScopeView(QAbstractScrollArea):
def __init__(self):
super().__init__()
self.set_my_viewport(ScopeView())
def set_my_viewport(self, new_viewport):
self.my_viewport = new_viewport
self.setViewport(self.my_viewport)
def viewportEvent(self, event):
# Uncommenting this breaks painting.
if event.type() == QEvent.Paint:
self.my_viewport.paintEvent(event)
return super().viewportEvent(event)
application = QApplication(sys.argv)
main_window = MainWindow()
main_window.show()
sys.exit(application.exec_())
Filed as a Qt bug: https://bugreports.qt.io/browse/QTBUG-53269
My previous answer was absolutely wrong. Kudos to the OP for his investigation.
Per the docs:
When inheriting QAbstractScrollArea, you need to do the following:
Control the scroll bars by setting their range, value, page step, and tracking their movements.
Draw the contents of the area in the viewport according to the values of the scroll bars.
Handle events received by the viewport in viewportEvent() - notably resize events.
Use viewport->update() to update the contents of the viewport instead of update() as all painting operations take place on the viewport.
Unless you need to do other event management, the very short viewportEvent() in your MCVE is correct. Take a look at the code (a better look than I did) and you'll see that most events (including paint events) are not passed to the viewport. Curiously, the code does make an exception to properly resize QOpenGLWidget.
I realize now the logic behind not painting by default is to allow you to update only the region of the viewport currently visible.
In short, the below is fine. I would recommend checking to ensure the paint event only includes the rect currently visible (check the value of rect() in the paint event), otherwise you'll be painting areas not currently visible in your viewport.
def viewportEvent(self, event):
# Uncommenting this breaks painting.
if event.type() == QEvent.Paint:
self.my_viewport.paintEvent(event)
return super().viewportEvent(event)
Apologies for my screwup. I hope this is helpful.
I'm writing a simple tool menu for Maya, and I'd like to stick it to the border of model panel (perspective).
from PySide import QtCore, QtGui
from maya import OpenMayaUI as omui
from shiboken import wrapInstance
class TestWidget(QtGui.QWidget):
def __init__(self, parent=None):
QtGui.QWidget.__init__(self, parent = self.getMayaWindow())
self.setWindowFlags(QtCore.Qt.Tool | QtCore.Qt.FramelessWindowHint)
self.setFixedSize(100, 100)
panelPtr = omui.MQtUtil.findControl('modelPanel4')
panel = wrapInstance(long(panelPtr), QtGui.QWidget)
position = panel.mapToGlobal(panel.pos())
self.move(position.x(), position.y() + panel.geometry().height() / 2 - self.geometry().height() / 2)
mainLayout = QtGui.QVBoxLayout(self)
button = QtGui.QPushButton('CLOSE')
button.setFixedSize(80, 80)
button.clicked.connect(self.deleteLater)
mainLayout.addWidget(button)
def getMayaWindow(self):
omui.MQtUtil.mainWindow()
ptr = omui.MQtUtil.mainWindow()
return wrapInstance(long(ptr), QtGui.QWidget)
w = TestWidget()
w.show()
The main widget is positioned exactly where I want when it is created (horizontally on the left side of model panel, vertically - in the middle of model panel).
I need to reposition it accordingly when the model panel is resized, but model panel does not emit resized() signal. I'd appreciate any advise.
I've been trying many things to get this working yesterday. I did some additionnal researches today and came to this topic: cgsociety: Creating a floating button inside the viewport
In case of broken link, this is one of the answer:
You can use geometry but there are some issues with triggering
commands based on selection and the undo queue. If you want to go that
route, I would suggest looking into zooHud and zooTriggers (Part of
the zooToolbox)
If you are wanting actual GUI control parented to the viewport, mel
only offers hudslider, hudbutton, and headsUpMessage.
You can also use PyQt and parent in your own custom widgets/layouts or
whatever you want using something like this:
import maya.OpenMayaUI as apiUI import sip
from PyQt4 import QtGui
view = apiUI.M3dView()
apiUI.M3dView.getM3dViewFromModelPanel('modelPanel4', view)
viewWidget = sip.wrapinstance(long(view.widget()), QtCore.QObject)
global myBtn
myBtn = QtGui.QPushButton(viewWidget)
myBtn.setText('testing!')
myBtn.move(100, 100) #Relative to top-left corner of viewport myBtn.show()
You can do anything a full qt widget can do with that, so it's
extremely flexible. but it would require having PyQt installed, which
can be a barrier depending on your tools distribution.
I did a mix of this answer and your code:
from PySide import QtCore, QtGui
from maya import OpenMayaUI as omui
from shiboken import wrapInstance
class CustomQWidget(QtGui.QWidget):
def __init__(self, *args, **kwargs):
QtGui.QWidget.__init__(self, *args, **kwargs)
mainLayout = QtGui.QVBoxLayout(self)
closeButton = QtGui.QPushButton('CLOSE')
closeButton.setFixedSize(80, 40)
closeButton.clicked.connect(self.deleteLater)
helloButton = QtGui.QPushButton('HELLO')
helloButton.setFixedSize(80, 40)
helloButton.clicked.connect(self.printHello)
#Trying to fix glitchy background / Doesn't work, why?
#Is it because layouts don't have background?
p = self.palette()
p.setColor(self.backgroundRole(), QtCore.Qt.red)
self.setPalette(p)
self.setAttribute(QtCore.Qt.WA_StyledBackground, True)
##############################
mainLayout.addWidget(closeButton)
mainLayout.addWidget(helloButton)
def printHello(self):
print "Hello"
view = omui.M3dView()
omui.M3dView.getM3dViewFromModelPanel('modelPanel4', view) #Given the name of a model panel,
#get the M3dView used by that panel. If this fails, then a panel with the given name could not be located.
viewWidget = wrapInstance(long(view.widget()), QtGui.QWidget)
position = viewWidget.mapToGlobal(viewWidget.pos())
w = CustomQWidget(viewWidget)
w.move(0, viewWidget.geometry().height() / 2 - 100 / 2) #Relative to middle-left corner of viewport
w.show()
One of the issue I have it that the background of the widget is glitched:
If anyone knows why and how to fix it, I'll edit my answer with pleasure.
Else, when running this script from Maya's script editor, the widget follows the panel when it is resized.
I did fix such a problem, but not using Python/PyQt.
The problem itself is, that your Qt Widget is there. I have not found a way to make it not paint its background.
My solution was different: I derived from a Qt Layout, pushed all my widgets into that layout and used MQtUtil to get the QWidget of that modelPanel's modelEditor to attach the "real Qt layout" to it.
Heavy caveat that may make Python not suited: Maya doesn't expect "non-Maya" Layouts to be bound to "real-Maya" Widgets like modelEditors. So you need to listen to QEvents and find out when to destroy your layout, so Maya doesn't crash trying.
set autofillbackground True to fix your background painting issue
I have to create one screen in Qt in which I have to show a remote having lots of buttons in it and when user clicks some button on actual remote, corresponding button in the image get highlighted. So what I have done is, I have used QLabel and set the remote image as background image and then I have put small rectangular label for each button and filled them with semi transparent color and when user click button in actual remote label color changes, but by using this method lot of labels are getting used making code looking inefficient, so I was thinking of drawing on QLabel (which has a remote as background image) over buttons.
Can anybody suggest me, which API of Qt should I use, and how to follow up on this?
I believe QGraphics is the correct route for a completely custom graphical interface, but if you want to try something that doesn't require you to change too much of your existing approach, you can do a widget with a custom paint event:
This is written in PyQt but you can easily translate to Qt
from PyQt4 import QtCore, QtGui
class LabelButton(QtGui.QWidget):
clicked = QtCore.pyqtSignal()
def __init__(self, labelStr, pixStr, parent=None):
super(LabelButton, self).__init__(parent)
self.label = labelStr
self.pix = QtGui.QPixmap(pixStr)
def paintEvent(self, event):
super(LabelButton, self).paintEvent(event)
rect = event.rect()
painter = QtGui.QPainter(self)
painter.drawPixmap(rect, self.pix)
pos = (rect.bottomLeft()+rect.bottomRight()) / 2
pos.setY(pos.y()-10)
painter.drawText(pos, self.label)
painter.end()
def mousePressEvent(self, event):
event.accept()
self.clicked.emit()
def handleClick():
print "CLICK"
if __name__ == "__main__":
app = QtGui.QApplication([])
widget = LabelButton("A Text Label", "myImage.png")
widget.resize(600,400)
widget.show()
widget.raise_()
widget.clicked.connect(handleClick)
app.exec_()
This is a rough example. You can get more fine tuned with the drawing of the text. This widget takes a label string, and a picture path, and will paint the picture as the background, and the text as a label. You can do any number of things with this custom widget in both the paint event, and with custom signals and events.
I have used this code to Draw over Image in Label:
Image is loaded in Ui and the Code is as follows In paintevent
void ColorTab::paintEvent(QPaintEvent *e){
ui->lbl_capture_img->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
ui->Lbl_color_tab_WG->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored);
Cap_Image = QImage(ui->lbl_capture_img->pixmap()->toImage());
Lbl_Image = QImage(ui->Lbl_color_tab_WG->pixmap()->toImage());
QPainter painter_Lbl(&Lbl_Image);
QPainter painter_Cap(&Cap_Image);
QPen pen(Qt::white, 5, Qt::DotLine, Qt::RoundCap, Qt::RoundJoin);
painter_Lbl.setPen(pen);
painter_Cap.setPen(pen);
painter_Lbl.drawPolygon(blinkRect_Lbl);
painter_Cap.drawPolygon(blinkRect_Cap);
ui->lbl_capture_img->setPixmap(QPixmap::fromImage(Cap_Image));
ui->Lbl_color_tab_WG->setPixmap(QPixmap::fromImage(Lbl_Image));
painter_Cap.end();
painter_Lbl.end();
}
I'd like to change the QStyle::PM_TabBarTabHSpace property for a PyQt application. I read the Qt document for QStyle, but I'm not sure how to set this correctly in PyQt.
Non-working code:
style = QStyleFactory.create('Cleanlooks')
style.PM_TabBarTabHSpace = 5 # 5 pixels?
app.setStyle(style)
This code runs, but it doesn't change the padding on the tabbar tabs. I tried using stylesheets to change the tabbar padding, but that ruins the graphics drawing, so that none of the default look-feel stuff gets drawn (I don't want to reimplement all the ui drawing).
I think I might need to use QProxyStyle, but I can't find any examples of how to use this in PyQt4. Edit: It seems that PyQt doesn't have QProxyStyle, as from PyQt4.QtGui import QProxyStyle fails.
Can someone please post an example of changing the value of PM_TabBarTabHSpace? Thanks.
Edit Here is a skeleton code. Changing the PM_TabBarTabHSpace value doesn't do anything. :(
from PyQt4.QtGui import (QApplication, QTabWidget, QWidget,
QStyle, QStyleFactory)
def myPixelMetric(self, option=None, widget=None):
if option == QStyle.PM_TabBarTabHSpace:
return 200 # pixels
else:
return QStyle.pixelMetric(option, widget)
style = QStyleFactory.create('Windows')
style.pixelMetric = myPixelMetric
app = QApplication('test -style Cleanlooks'.split())
# Override style
app.setStyle(style)
tab = QTabWidget()
tab.addTab(QWidget(), 'one')
tab.addTab(QWidget(), 'two')
tab.show()
app.exec_()
QStyle.pixelMetric(...) is built-in class method. You can not set via function pointing. Because, it is in C code. You can test it with adding
def myPixelMetric(self, option=None, widget=None):
print 'Debug, i am calling'
...
in your myPixelmetric function. You need to subclass Style object to achieve this. Here is an example:
class MyStyle(QCommonStyle):
def pixelMetric(self, QStyle_PixelMetric, QStyleOption_option=None, QWidget_widget=None):
if QStyle_PixelMetric == QStyle.PM_TabBarTabHSpace:
return 200
else:
return QCommonStyle.pixelMetric(self, QStyle_PixelMetric, QStyleOption_option, QWidget_widget)
app = QApplication('test -style Cleanlooks'.split())
app.setStyle(MyStyle())
This code snippet will work, but it is ugly. I prefer using stylesheets over manipulating Style.