PyQt4: QLabel with clear button - python

First I'll show the code.
class XLineEdit(QtGui.QLineEdit):
'''QLineEdit with clear button, which appears when user enters text.'''
def __init__(self, pixmap, parent=None):
QtGui.QLineEdit.__init__(self, parent)
self.layout = QtGui.QHBoxLayout(self)
self.image = QtGui.QLabel(self)
self.image.setCursor(QtCore.Qt.ArrowCursor)
self.image.setFocusPolicy(QtCore.Qt.NoFocus)
self.image.setStyleSheet("border: none;")
self.image.setPixmap(pixmap)
self.image.setSizePolicy(
QtGui.QSizePolicy.Expanding,
QtGui.QSizePolicy.Expanding)
self.image.adjustSize()
self.image.setScaledContents(True)
self.layout.addWidget(
self.image, alignment=QtCore.Qt.AlignRight)
self.textChanged.connect(self.changed)
self.image.hide()
def changed(self, text):
if len(text) > 0:
self.image.show()
else: # if entry is empty
self.image.hide()
That creates QLineEdit object with custom button from QLabel at the right side of QLineEdit. I have only two problems:
If I change the font of XLineEdit ("XLineEdit object".setFont(QFont)), image button will look good by vertical, but will look ugly by horizontal. It seems that vertical size changes on changing the size of QLineEdit's font, but horizontal size is not. How can I fix this? Is there any other way to create QLineEdit with clear button? I've tried to create QPushButton with custom QIcon, but icon doesn't change it's size at all (neither vertical, nor horizontal).
How can I create a new signal when user clicks on QLabel? It seems that there is no analog for QPushButton's 'clicked'.
Thanks!

While #reclosedev already commented on your question with a link to a C++ example for the clear button aspect, I wanted to add information about your second question...
You can create a clickable QLabel by overloading the MousePressEvent and emitting your own custom signal.
from PyQt4.QtCore import pyqtSignal
from PyQt4.QtGui import QLabel, QStyle
class ClickLabel(QLabel):
clicked = pyqtSignal()
def __init__(self, *args, **kwargs)
super(ClickLabel, self).__init__(*args, **kwargs)
def mousePressEvent(self, event):
event.accept()
self.clicked.emit()
A comment about the C++ link that was provided in the other comment. Instead of using an HBoxLayout, they are just directly parenting the button to the QLabel widget, and using the resizeEvent to always move it to the right side of the QLabel.

Related

PyQt5 QWidget event blocks other events. How to avoid it?

I have the next problem:
A created a custom widget (simple derived QWidget class). When I middle-mouse click on it - it creates another QWidget class (sort of context menu). When I release middle-mouse button - that context widget disappears. So that works. What does not work is - that context widget also has some content added, like other small widgets, icons, etc and they all have their own custom events (simple example - enterEvent and leaveEvent with prints indicating those events). But those inner widget events are not working, they are blocked while I keep middle-mouse pressed. When I release it - context widget disappears. Would like to know if there is any solution to let inner widgets'events work as expected.
Here is a minimal example where inner widget does not run mouse events as they are blocked by MainWidget event:
from PyQt5 import QtWidgets, QtGui, QtCore
class SomeSmallWidget(QtWidgets.QWidget):
def __init__(self, increment=1, globalValue=0):
super(SomeSmallWidget, self).__init__()
# UI
self.setMinimumWidth(40)
self.setMaximumWidth(40)
self.setMinimumHeight(40)
self.setMaximumHeight(40)
self.mainLayout = QtWidgets.QVBoxLayout()
self.setLayout(self.mainLayout)
def enterEvent(self, event):
print('Entered') # NOT WORKING
super(SomeSmallWidget, self).enterEvent(event)
def leaveEvent(self, event):
print('Leaved') # NOT WORKING
super(SomeSmallWidget, self).leaveEvent(event)
class ContextWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(ContextWidget, self).__init__()
self.setMouseTracking(True)
# position
point = parent.rect().topLeft()
global_point = parent.mapToGlobal(point)
self.move(global_point - QtCore.QPoint(0, 0))
self.innerWidget = SomeSmallWidget() # Here is that small widget, which event does not work
self.mainLayout = QtWidgets.QVBoxLayout()
self.setLayout(self.mainLayout)
self.mainLayout.addWidget(self.innerWidget)
class MainWidget(QtWidgets.QLineEdit):
def __init__(self, value='0'):
super(MainWidget, self).__init__()
self.setMouseTracking(True)
self.popMenu = None
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.MiddleButton: # while we keep MMB pressed - we see context widget
self.popMenu = ContextWidget(parent=self)
self.popMenu.show()
super(MainWidget, self).mousePressEvent(event)
def mouseReleaseEvent(self, event):
if event.button() == QtCore.Qt.MiddleButton:
if self.popMenu:
self.popMenu = None
Events are not blocked, all mouse events are sent to widget that was under cursor when mouse button was pressed. Usually (always) it makes more sense. Imagine two buttons next to each other. Suppose user pressed one, moved cursor and released over second button. What was his intention? He probably changed his mind. If button triggers action on mouse press - this options will not be available and it's probably too soon, if button triggers action on mouse release, which button should recieve mouserelease event? If we send mouseevent to second button - that was not pressed it will trigger action that used didn't want. If we dont send mouserelease event to first button - it will stay in sunken mode. Imagine user is selecting text in lineedit, and while selecting he leaves lineedit and goes to other widgets, should they react somehow, and should focus be switched? Probably not. So there is only one active window and only one focused widget at a time and it receives keyboard and mouse input and reacts to it. Most of menus are shown after mouserelease and closes on next mouseclick, providing better user experience.
However, If you still want your widget to receive mouse events, you can achive this by translating it from ContextWidget to SomeSmallWidget like this:
class SomeSmallWidget(QtWidgets.QWidget):
...
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.fillRect(self.rect(), QtCore.Qt.blue)
def onMouseEnter(self):
print('onMouseEnter')
def onMouseLeave(self):
print('onMouseLeave')
class MainWidget(QtWidgets.QLineEdit):
...
def mouseMoveEvent(self, event):
if self.popMenu:
self.popMenu.mouseTest(event.globalPos())
class ContextWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
...
self._inside = False
def mouseTest(self, p):
widget = self.innerWidget
rect = QtCore.QRect(widget.mapToGlobal(QtCore.QPoint(0,0)), widget.size())
inside = rect.contains(p)
if inside != self._inside:
if inside:
widget.onMouseEnter()
else:
widget.onMouseLeave()
self._inside = inside
Notice I added paintEvent to see the widget bounds.

QTooltip with absolute position that still moves relative to parent

I'm building a custom combobox, from a (subclassed) QLineEdit and QListWidget for the dropdown menu
I'm setting the window flags to QTool so that its a floating window but doesnt steal focus from the lineedit (since the user needs to be able to input text to filter the list). This works fine but the list is now completely detached from the parent widget, so I can drag the top menu bar and move it away from the list which I don't want.
Is there a way to use QTool or QTooltip but keep it parented to a widget?
One other method would be setting the window flags to QPopup, in which case the popup closes when the top menu bar is clicked so cannot be dragged away. However with QPopup it steals focus from the line edit
Below is a simple example illustrating the issue:
from PySide2 import QtCore, QtWidgets, QtGui
import sys
class LineEditClickable(QtWidgets.QLineEdit):
"""Custom QLineEdit to detect clicked, focus and key events
Signals: clicked, focusOut, arrowUp, arrowDown
"""
clicked = QtCore.Signal(QtGui.QMouseEvent)
def __init__(self, value=''):
super(LineEditClickable, self).__init__(value)
# remove border on Mac
self.setAttribute(QtCore.Qt.WA_MacShowFocusRect, 0)
self.setFocusPolicy(QtCore.Qt.ClickFocus)
def mousePressEvent(self, event):
"""Emit clicked signal"""
self.clicked.emit(event)
super(LineEditClickable, self).mousePressEvent(event)
class popup(QtWidgets.QWidget):
def __init__(self, parent = None, widget=None):
QtWidgets.QWidget.__init__(self, parent)
layout = QtWidgets.QVBoxLayout(self)
self.list = QtWidgets.QListWidget()
layout.addWidget(self.list)
# adjust the margins or you will get an invisible, unintended border
layout.setContentsMargins(0, 0, 0, 0)
self.adjustSize()
# tag this widget as a popup
self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.Tool)
# self.setWindowFlags(QtCore.Qt.Popup)
def update(self, widget):
# calculate the botoom right point from the parents rectangle
point = widget.rect().bottomRight()
# map that point as a global position
global_point = widget.mapToGlobal(point)
# by default, a widget will be placed from its top-left corner, so
# we need to move it to the left based on the widgets width
self.move(global_point - QtCore.QPoint(self.width(), 0))
def show_popup(self, widget):
self.update(widget)
self.show()
class Window(QtWidgets.QWidget):
def __init__(self):
QtWidgets.QWidget.__init__(self)
self.le = LineEditClickable(self)
self.le.clicked.connect(self.handleOpenDialog)
self.le.move(250, 50)
self.resize(600, 200)
self.popup = popup(self, self.le)
self.popup.list.addItems(['one','two','three'])
def handleOpenDialog(self):
self.popup.show_popup(self.le)
self.popup.show()
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec_())```
The basic answer to your question is to use the correct flags and focus options.
If you look at how QCompleter implements setPopup(), you'll see the following:
popup->setWindowFlag(Qt::Popup);
popup->setFocusPolicy(Qt::NoFocus);
[...]
popup->setFocusProxy(d->widget);
As you've already experienced, Tool is not a good option: while avoids stealing focus from the line edit, it also has issues with any mouse click that happens outside the UI.
If you still want to use Tool, you could update the widget position, by installing an event filter on the top level window of the line edit and intercept its move events, but it's not guaranteed that it works and totally depends on the platform you're using it. For example, on certain Linux window managers you only receive it when the mouse is released after dragging the window.
class popup(QtWidgets.QWidget):
_widget = None
_window = None
# ...
def show_popup(self, widget):
if self._window:
self._window.removeEventFilter(self)
self.update(widget)
self.show()
self._widget = widget
self._window = widget.window()
self._window.installEventFilter(self)
def hideEvent(self, event):
if self._window:
self._window.removeEventFilter(self)
def closeEvent(self, event):
if self._window:
self._window.removeEventFilter(self)
def eventFilter(self, source, event):
if event.type() == QtCore.QEvent.Move:
self.update(self._widget)
return super().eventFilter(source, event)
Frankly, I'd suggest you to use what Qt already provides you without trying to reinvent the wheel. In your case, use a QCompleter and reimplement what you need for it's popup.
Note that if you want to show all items when the line edit gets focus and there's no text yet, you could change the completion mode.
class LineEdit(QtWidgets.QLineEdit):
def __init__(self, *args, **kwargs):
# ...
self.textChanged.connect(self.showCompleter)
def showCompleter(self):
completer = self.completer()
if not completer:
return
if not self.text():
completer.setCompletionMode(completer.UnfilteredPopupCompletion)
else:
completer.setCompletionMode(completer.PopupCompletion)
completer.complete()
You might want to do the same also in the keyPressEvent override, after calling the base class implementation and ensuring that the popup is not yet visible.

PyQt4: Trouble making a custom dock TitleBarWidget

I was hoping someone could help me with creating a custom title bar widget for a dock widget in a PyQt4 GUI program. All I want to do is emulate the exact same look and function of the default title bar, but with an extra, custom button. I couldn't find an easy way to do this as I don't know if there's a default title bar widget I can add stuff to, so I made a custom dock title bar widget:
from PyQt4 import QtGui, QtCore
class DockTitleBar(QtGui.QFrame):
def __init__(self, parent):
super(DockTitleBar, self).__init__(parent)
# Is this the only way to give the title bar a border?
self.setFrameStyle(QtGui.QFrame.Raised | QtGui.QFrame.StyledPanel)
# Layout for title box
layout = QtGui.QHBoxLayout(self)
layout.setSpacing(1)
layout.setMargin(1)
self.label = QtGui.QLabel(parent.windowTitle())
icon_size = QtGui.QApplication.style().standardIcon(
QtGui.QStyle.SP_TitleBarNormalButton).actualSize(
QtCore.QSize(100, 100))
button_size = icon_size + QtCore.QSize(5, 5)
# Custom button I want to add
self.button = QtGui.QToolButton(self)
self.button.setAutoRaise(True)
self.button.setMaximumSize(button_size)
self.button.setIcon(QtGui.QApplication.style().standardIcon(
QtGui.QStyle.SP_TitleBarContextHelpButton))
self.button.clicked.connect(self.do_something)
# Close dock button
self.close_button = QtGui.QToolButton(self)
self.close_button.setAutoRaise(True)
self.close_button.setMaximumSize(button_size)
self.close_button.setIcon(QtGui.QApplication.style().standardIcon(
QtGui.QStyle.SP_DockWidgetCloseButton))
self.close_button.clicked.connect(self.close_parent)
# Setup layout
layout.addWidget(self.label)
layout.addStretch()
layout.addWidget(self.button)
layout.addWidget(self.close_button)
def do_something(self):
# Do something when custom button is pressed
pass
def close_parent(self):
self.parent().hide()
It seems to work okay, except for when the dock is dragged around in its floating state. Normally there are borders and even the title bar is highlighted, but with my janky version there is no frame for the floating dock so it's hard to tell where it is, and the title bar isn't highlighted. Is there something I can fix/add or should I be doing this in an entirely different way?

Hover issue in PyQt

I want to do hover. I saw an example and then write a script which will be use as I made program. I am facing one problem that hover only occur if you put mouse on the left corner of button. I want that it will happen for all the button that if i move cursor on button then it should change.
Here is my code:
from PyQt4 import QtGui, QtCore
from PyQt4.QtCore import pyqtSignal
import os,sys
class HoverButton(QtGui.QToolButton):
def enterEvent(self,event):
print("Enter")
button.setStyleSheet("background-color:#45b545;")
def leaveEvent(self,event):
button.setStyleSheet("background-color:yellow;")
print("Leave")
app = QtGui.QApplication(sys.argv)
widget = QtGui.QWidget()
button = QtGui.QToolButton(widget)
button.setMouseTracking(True)
buttonss = HoverButton(button)
button.setIconSize(QtCore.QSize(200,200))
widget.show()
sys.exit(app.exec_())
Is this what you're looking for
from PyQt4 import QtGui, QtCore
from PyQt4.QtCore import pyqtSignal
import os,sys
class Main(QtGui.QWidget):
def __init__(self, parent=None):
super(Main, self).__init__(parent)
layout = QtGui.QVBoxLayout(self) # layout of main widget
button = HoverButton(self)
button.setIconSize(QtCore.QSize(200,200))
layout.addWidget(button) # set your button to the widgets layout
# this will size the button nicely
class HoverButton(QtGui.QToolButton):
def __init__(self, parent=None):
super(HoverButton, self).__init__(parent)
self.setMouseTracking(True)
def enterEvent(self,event):
print("Enter")
self.setStyleSheet("background-color:#45b545;")
def leaveEvent(self,event):
self.setStyleSheet("background-color:yellow;")
print("Leave")
app = QtGui.QApplication(sys.argv)
main = Main()
main.show()
sys.exit(app.exec_())
In your code you had a button in a button and the nested button wasn't assigned to a QLayout widget. Although, I'm not sure why you're adding a button inside of a button. One thing that I've learned from working with GUI's is that it's so much easier if you modularize your code. Now you can take this custom button and apply it somewhere else.
You should use the stylesheet as
QToolButton:hover
{
background-color: rgb(175,175,175);
}
You probably want focus and blur, rather than enter and leave will only fire when the mouse actually enters or leaves the boundary of the button and will probably just be short duration impulses rather than toggles. focus and blur will toggle with the hover.

uncheck radiobutton - PyQt4

In this sample of code:
from PyQt4.QtGui import QDialog, QPushButton, QRadioButton, QHBoxLayout, QApplication, QButtonGroup
import sys
class Form(QDialog):
def __init__(self, parent=None):
super(Form, self).__init__(parent=None)
button = QPushButton('Button')
self.radiobutton1 = QRadioButton('1')
self.radiobutton2 = QRadioButton('2')
#self.group = QButtonGroup()
#self.group.addButton(self.radiobutton1)
#self.group.addButton(self.radiobutton2)
#self.group.setExclusive(False)
layout = QHBoxLayout()
layout.addWidget(button)
layout.addWidget(self.radiobutton1)
layout.addWidget(self.radiobutton2)
self.setLayout(layout)
button.clicked.connect(self.my_method)
def my_method(self):
self.radiobutton1.setChecked(False)
self.radiobutton2.setChecked(False)
app = QApplication(sys.argv)
form = Form()
form.show()
app.exec_()
When the button clicked I expect the selected radioButton to be unchecked, but that never happens. If I uncomment the comment lines and run the code, then I can uncheck radioButtons. But another problem occurs. Because the group is not exclusive, I can set both radioButtons checked something that must not happens.
What should I do to be able to unckeck the buttons while only one button at a time can be selected?
This feels like cheating, but it works:
import sys
import PyQt4.QtGui as QtGui
class Form(QtGui.QDialog):
def __init__(self, parent=None):
super(Form, self).__init__(parent)
button = QtGui.QPushButton('Button')
button.clicked.connect(self.my_method)
self.radiobutton1 = QtGui.QRadioButton('1')
self.radiobutton2 = QtGui.QRadioButton('2')
layout = QtGui.QHBoxLayout()
layout.addWidget(button)
layout.addWidget(self.radiobutton1)
layout.addWidget(self.radiobutton2)
self.setLayout(layout)
self.group = QtGui.QButtonGroup()
self.group.addButton(self.radiobutton1)
self.group.addButton(self.radiobutton2)
def my_method(self):
self.group.setExclusive(False)
self.radiobutton1.setChecked(False)
self.radiobutton2.setChecked(False)
self.group.setExclusive(True)
app = QtGui.QApplication(sys.argv)
form = Form()
form.show()
app.exec_()
As you've pointed out, when self.group.setExclusive(False) is set, you can untoggle both radio buttons.
And when self.group.setExclusive(True), only one radio button can be set.
So my_method simply calls self.group.setExclusive(False) so it can unset both radio buttons, then resets self.group.setExclusive(True).
PS. I think parent should not be set to None on this line:
super(Form, self).__init__(parent = None)
since if a non-trivial parent is sent to Form, you would probably want to pass that parent on to QDialog.__init__.
To anyone looking for a simple fix to this very annoying problem, connect each button to a slot that controls the CheckState of the other buttons.
Simply add the list of buttons you want to a QButtonGroup, get the list of buttons, check that the sender is not the same button, and uncheck others.
Assuming that you instantiate your buttons in a loop, you can easily implement this:
self.bg = QButtonGroup()
self.bg.setExclusive(False)
for button in list_of_buttons:
self.bg.addButton(button)
button.clicked.connect(self.uncheck_other_buttons)
def uncheck_other_btns(self):
for button in self.bg.buttons(): # returns the list of all added buttons
if self.sender() != button: # in PyQt5, button.objectName() fails if name isn't set,
# instead, simply check that the signal sender() object
# is not the same object as the clicked button
button.setChecked(False) # then set all other buttons to be unchecked

Categories