Apologies if the question is confusing, I'm not the best at wording stuff. I'll try to explain further if required.
Background: I'm creating a simple image viewer that gives the user the ability to cycle through their library of images. I wanted to give the user the ability to browse through their image library (basically a file browser) so I tried creating a little browser that dynamically added QLabels with pixmap for each image in the library.
Problem: First my window crashed without loading any image labels, but since my image library is pretty huge I thought limiting the number would help. The results weren't so different; with the limitation (of only 4 images) the image labels displayed but anytime I try to do anything in the window (resize the window, resize the dock) it crashes. What am I doing wrong here?
Code:
import sys
import os
import time
from PyQt5 import QtWidgets as qtw
from PyQt5 import QtGui as qtg
from PyQt5 import QtCore as qtc
from PIL import ImageQt, Image
import memory_profiler
from DBManager import DBBrowser
class MainWindow(qtw.QMainWindow):
resize_constant = 10
def __init__(self):
self.root = r'Images'
self.control_modifier = False
self.resized = 0
super().__init__()
self.main_widget = qtw.QWidget()
self.main_widget.setLayout(qtw.QVBoxLayout())
self.main_widget.layout().setSpacing(0)
self.setCentralWidget(self.main_widget)
# self.setWindowFlag(qtc.Qt.FramelessWindowHint)
self.create_main_window()
self.viewer()
self.menubar()
self.main_widget.layout().addWidget(self.menu_widget)
self.main_widget.layout().addWidget(self.main_window)
self.main_widget.layout().setContentsMargins(0,0,0,0)
self.show()
def menubar(self):
self.menu_widget = qtw.QWidget()
self.menu_widget.setLayout(qtw.QHBoxLayout())
self.menu_widget.layout().setContentsMargins(0,0,0,0)
self.menu_widget.layout().setSpacing(0)
self.menu_widget.layout().setAlignment(qtc.Qt.AlignLeft)
import_btn = qtw.QPushButton('Import', clicked=self.import_database)
explorer_btn = qtw.QPushButton('Explorer', clicked=self.show_hide_explorer)
import_btn.setFixedWidth(70)
explorer_btn.setFixedWidth(70)
self.menu_widget.layout().addWidget(import_btn)
self.menu_widget.layout().addWidget(explorer_btn)
import_btn.setFocusPolicy(qtc.Qt.ClickFocus)
explorer_btn.setFocusPolicy(qtc.Qt.ClickFocus)
def create_main_window(self):
self.main_window = qtw.QMainWindow()
self.dock = Dock('Explorer')
self.explorer = Explorer()
self.dock.setWidget(self.explorer)
self.dock.setTitleBarWidget(qtw.QWidget())
self.main_window.setFocusPolicy(qtc.Qt.StrongFocus)
self.main_window.setFocus()
self.main_window.addDockWidget(qtc.Qt.LeftDockWidgetArea, self.dock)
def viewer(self):
self.image_handler()
self.image_label = qtw.QLabel(alignment=qtc.Qt.AlignCenter)
self.curr_img_name = self.images[0]
self.curr_img_org = Image.open(os.path.join(self.root, self.curr_img_name))
self.curr_img_qt = ImageQt.ImageQt(self.curr_img_org)
self.curr_img_pixmap = qtg.QPixmap.fromImage(self.curr_img_qt)
self.image_label.setPixmap(self.curr_img_pixmap)
self.image_scrollarea = ImageScrollArea(self)
self.main_window.setCentralWidget(self.image_scrollarea)
self.image_scrollarea.setWidget(self.image_label)
self.image_scrollarea.setWidgetResizable(True)
self.image_scrollarea.setVerticalScrollBarPolicy(qtc.Qt.ScrollBarAlwaysOff)
self.image_scrollarea.setHorizontalScrollBarPolicy(qtc.Qt.ScrollBarAlwaysOff)
self.image_scrollarea.setFocusPolicy(qtc.Qt.NoFocus)
def image_handler(self):
self.images = sorted([(int(image.split(os.extsep)[0]), image.split(os.extsep)[1]) for image in os.listdir(self.root)
if os.path.isfile(os.path.join(self.root, image)) and not image.endswith('.csv')])
self.images = ['.'.join((str(image[0]), image[1])) for image in self.images]
def resize_image(self):
self.curr_img = self.curr_img_org.copy()
self.curr_img_qt = ImageQt.ImageQt(self.curr_img)
self.curr_img_pixmap = qtg.QPixmap.fromImage(self.curr_img_qt)
if self.resized < 0:
self.curr_img.thumbnail((
int(self.curr_img_org.width - (abs(self.resized)/100 * self.curr_img_org.width))
, int(self.curr_img_org.height - (abs(self.resized)/100 * self.curr_img_org.height)))
, Image.BOX)
self.curr_img_qt = ImageQt.ImageQt(self.curr_img)
self.curr_img_pixmap = qtg.QPixmap.fromImage(self.curr_img_qt)
elif self.resized > 0:
self.curr_img = self.curr_img_org.copy()
self.curr_img = self.curr_img.resize((
int((abs(self.resized)/100 * self.curr_img_org.width) + self.curr_img_org.width)
, int((abs(self.resized)/100 * self.curr_img_org.height) + self.curr_img_org.height))
, Image.BOX)
self.curr_img_qt = ImageQt.ImageQt(self.curr_img)
self.curr_img_pixmap = qtg.QPixmap.fromImage(self.curr_img_qt)
def change_image(self, direction):
current_index = self.images.index(self.curr_img_name)
if direction == qtc.Qt.Key_Left:
if current_index == 0:
return
self.curr_img_name = self.images[current_index-1]
self.curr_img_org = Image.open(os.path.join(self.root, self.curr_img_name))
self.curr_img_qt = ImageQt.ImageQt(self.curr_img_org)
self.curr_img_pixmap = qtg.QPixmap.fromImage(self.curr_img_qt)
if self.resized != 0:
self.resize_image()
self.image_label.setPixmap(self.curr_img_pixmap)
self.image_scrollarea.scroll_value = 0
self.image_scrollarea.verticalScrollBar().setValue(self.image_scrollarea.scroll_value)
if direction == qtc.Qt.Key_Right:
if current_index == len(self.images)-1:
return
self.curr_img_name = self.images[current_index+1]
self.curr_img_org = Image.open(os.path.join(self.root, self.curr_img_name))
self.curr_img_qt = ImageQt.ImageQt(self.curr_img_org)
self.curr_img_pixmap = qtg.QPixmap.fromImage(self.curr_img_qt)
if self.resized != 0:
self.resize_image()
self.image_label.setPixmap(self.curr_img_pixmap)
self.image_scrollarea.scroll_value = 0
self.image_scrollarea.verticalScrollBar().setValue(self.image_scrollarea.scroll_value)
def import_database(self):
file_location = qtw.QFileDialog.getOpenFileName(self, 'Import database', os.path.abspath('.'), 'database files (*.db)')
self.explorer.set_database_location(file_location[0])
self.explorer.offset = 0
gallery_data_dict_list = self.explorer.get_items()
self.explorer.set_items(gallery_data_dict_list)
def show_hide_explorer(self):
if self.dock.isVisible():
self.dock.hide()
else:
self.dock.show()
def keyPressEvent(self, event):
if event.key() == qtc.Qt.Key_Left:
self.change_image(qtc.Qt.Key_Left)
if event.key() == qtc.Qt.Key_Right:
self.change_image(qtc.Qt.Key_Right)
if event.key() == qtc.Qt.Key_Up:
self.image_scrollarea.scroll_(120)
if event.key() == qtc.Qt.Key_Down:
self.image_scrollarea.scroll_(-120)
if event.key() == qtc.Qt.Key_Escape:
if self.dock.isVisible():
self.dock.hide()
if event.key() == qtc.Qt.Key_Control:
self.control_modifier = True
if self.control_modifier:
if event.key() == qtc.Qt.Key_E:
self.show_hide_explorer()
def keyReleaseEvent(self, event):
if event.key() == qtc.Qt.Key_Control:
self.control_modifier = False
def wheelEvent(self, event):
if self.control_modifier:
if event.angleDelta().y() == 120:
self.resized += 10
if self.resized >= 90: self.resized = 90
self.resize_image()
self.image_label.setPixmap(self.curr_img_pixmap)
elif event.angleDelta().y() == -120:
self.resized -= 10
if self.resized <= -90: self.resized = -90
self.resize_image()
self.image_label.setPixmap(self.curr_img_pixmap)
class ImageScrollArea(qtw.QScrollArea):
def __init__(self, main_window):
super().__init__()
self.main_window = main_window
self.scroll_value = self.verticalScrollBar().minimum()
self.scroll_constant = 100
def wheelEvent(self, event):
if self.main_window.control_modifier:
event.ignore()
else:
self.scroll_(event.angleDelta().y())
def scroll_(self, y):
if y == -120:
if self.scroll_value + self.scroll_constant >= self.verticalScrollBar().maximum():
self.scroll_value = self.verticalScrollBar().maximum()
else:
self.scroll_value += self.scroll_constant
elif y == 120:
if self.scroll_value - self.scroll_constant <= self.verticalScrollBar().minimum():
self.scroll_value = self.verticalScrollBar().minimum()
else:
self.scroll_value -= self.scroll_constant
self.verticalScrollBar().setValue(self.scroll_value)
class Dock(qtw.QDockWidget):
def __init__(self, title):
super().__init__()
self.setWindowTitle(title)
class Explorer(qtw.QWidget):
def __init__(self):
super().__init__()
# self.database_location = None
self.setLayout(qtw.QVBoxLayout())
self.search_box = qtw.QWidget()
self.search_box.setFixedHeight(int(0.15*self.height()))
self.search_box.setLayout(qtw.QFormLayout())
self.layout().addWidget(self.search_box)
search_edit = qtw.QLineEdit()
search_edit.setPlaceholderText('Enter search term(s)...')
search_bar = qtw.QHBoxLayout()
filter_options = ['tags', 'people', 'date', 'custom...']
filter_option_combobox = qtw.QComboBox()
filter_option_combobox.addItems(filter_options)
filter_option_combobox.setCurrentIndex(0)
filter_option_combobox.currentTextChanged.connect(self.custom_filters)
search_btn = qtw.QPushButton('Search')
search_bar.addWidget(filter_option_combobox)
search_bar.addWidget(search_btn)
self.search_box.layout().addRow(search_edit)
self.search_box.layout().addRow(search_bar)
self.browser()
self.layout().addWidget(self.browser_scrollarea)
def custom_filters(self, value):
if value == 'custom...':
pass
def browser(self):
self.browser_scrollarea = qtw.QScrollArea()
self.browser_scrollarea.setVerticalScrollBarPolicy(qtc.Qt.ScrollBarAlwaysOn)
self.browser_scrollarea.setHorizontalScrollBarPolicy(qtc.Qt.ScrollBarAlwaysOn)
def get_items(self):
self.DBB = DBBrowser()
self.DBB.set_database(database_location=self.database_location)
self.DBB.set_filters()
results = self.DBB.sqlite_select(get='*', order_by='date', order_in='ASC', table='Library')
self.offset += 10
gallery_data_dict_list = [{
'location' : result[0],
'date' : result[1],
'tags' : result[2]
} for result in results]
return gallery_data_dict_list
def set_items(self, gallery_data_dict_list):
self.browser_widget = qtw.QWidget()
self.browser_widget.setLayout(qtw.QGridLayout())
key = 0
for gallery_data_dict in gallery_data_dict_list:
if key > 3:
break
description = qtw.QScrollArea()
text = qtw.QLabel('texttexttexttexttexttexttexttexttexttexttexttexttexttexttexttexttex')
description.setWidget(text)
thumbnail = Image.open(os.path.join(gallery_data_dict['location'], os.listdir(gallery_data_dict['location'])[0]))
thumbnail.thumbnail((250,250))
thumbnail = ImageQt.ImageQt(thumbnail)
self.thumbnail_pixmap = qtg.QPixmap.fromImage(self.thumbnail)
self.thumb = qtw.QLabel(pixmap=self.thumbnail_pixmap)
# text.setFixedWidth(int(self.browser_scrollarea.width()*50/100))
self.browser_widget.layout().addWidget(self.thumb, key, 0)
self.browser_widget.layout().addWidget(description, key, 1)
key += 1
self.browser_scrollarea.setWidget(self.browser_widget)
def set_database_location(self, location):
self.database_location = location
if __name__ == '__main__':
app = qtw.QApplication(sys.argv)
mw = MainWindow()
sys.exit(app.exec_())
Edit:
Following #musicamante's comment, I tried a couple of things to reprex my question and it doesn't seem like anything but a random occurrence with a 99.999% fail rate. I don't say 100% because the same code that crashed a million times worked fine once. I tried stripping the code to nothing but just a QWidget with two widgets; a button to load the browser, and the browser. Nevertheless, it still crashed. I don't know how much more minimal I can go.
Then I realized, I was using PIL.Image and PIL.ImageQt instead of QPixmap. Changing all my images to use qt's pixmap instead fixed the problem, as far as my experience went. I tried using PIL.Image for the main viewer and QPixmap for the browser but it still crashed.
My question now is, is there a way I can make it work well with PIL? Because, unless I'm missing something, I feel like PIL.Image is a lot more feature rich for image manipulation than QPixmap. Namely resizing the image (PIL has a ton of algorithms but QPixmap only has Qt.FastTransformation and Qt.SmoothTransformation as far as I'm aware) and especially the .copy() function. Or alternatively, is there some other library?
Related
I would like to have a scrollable context menu so that I can place many Actions in it. I saw an answer from another post that setting menu.setStyleSheet('QMenu{menu-scrollable: 1;}') will enable scroll Bar but it doesn't seem to do the job.
Here is a demonstration of the context menu of the blender software.
How can this be achieved?
Dealing with menu customization (with any framework) is not an easy task, and from experience I can telly you that trying to use simple approaches such as toggling item visibility will most certainly result in unexpected behavior and issues in user experience.
Three aspects must be kept in mind:
what you see on your screen is never what the final user will see;
there's always a certain amount of scenarios that you didn't consider, mostly due to "lazy debugging" which leads you to always test just a handful of situations without being thorough enough;
menus have been around for decades, and users are very accustomed to them, they know very well how they work and behave (even if unconsciously), and unusual behavior or visual hint can easily bring confusion and get them annoyed;
From your given answer, I can see at least the following important issues:
there are serious problems in the way geometries and visibilities are dealt with, with the result that some items are visible even when they shouldn't;
menu items can (and should) be programmatically hidden, resulting in unexpected behavior (especially since you might restore the visibility on a previously hidden item);
items with very long text are not considered, and will be cropped;
keyboard navigation is not supported, so the user can navigate to an invisible item;
the arrows are misleading, since they overlap items and there's no hint about possible further scrolling (I know that this is also the way Qt normally behaves, but that's not the point);
no "hover" scrolling is implemented, so a partially hidden item will result in a "highlighted arrow", which will lead the user to think that clicking will result in scrolling;
The solution, "unfortunately", is to correctly implement everything is needed, starting from painting and, obviously, user interaction.
The following is an almost complete implementation of a scrollable menu; the scrolling can be enabled by setting a maximum height or a maxItemCount keyword argument that guesses the height based on a standard item; it is then activated by moving on the arrows (and/or clicking on them) as well as using keyboard arrows.
It's not perfect yet, there are probably some aspect I didn't consider (see the above "lazy debugging" note), but for what I can see it should work as expected.
And, yes, I know, it's really extended; but, as said, menus are not as simple as they look.
class ScrollableMenu(QtWidgets.QMenu):
deltaY = 0
dirty = True
ignoreAutoScroll = False
def __init__(self, *args, **kwargs):
maxItemCount = kwargs.pop('maxItemCount', 0)
super().__init__(*args, **kwargs)
self._maximumHeight = self.maximumHeight()
self._actionRects = []
self.scrollTimer = QtCore.QTimer(self, interval=50, singleShot=True, timeout=self.checkScroll)
self.scrollTimer.setProperty('defaultInterval', 50)
self.delayTimer = QtCore.QTimer(self, interval=100, singleShot=True)
self.setMaxItemCount(maxItemCount)
#property
def actionRects(self):
if self.dirty or not self._actionRects:
self._actionRects.clear()
offset = self.offset()
for action in self.actions():
geo = super().actionGeometry(action)
if offset:
geo.moveTop(geo.y() - offset)
self._actionRects.append(geo)
self.dirty = False
return self._actionRects
def iterActionRects(self):
for action, rect in zip(self.actions(), self.actionRects):
yield action, rect
def setMaxItemCount(self, count):
style = self.style()
opt = QtWidgets.QStyleOptionMenuItem()
opt.initFrom(self)
a = QtWidgets.QAction('fake action', self)
self.initStyleOption(opt, a)
size = QtCore.QSize()
fm = self.fontMetrics()
qfm = opt.fontMetrics
size.setWidth(fm.boundingRect(QtCore.QRect(), QtCore.Qt.TextSingleLine, a.text()).width())
size.setHeight(max(fm.height(), qfm.height()))
self.defaultItemHeight = style.sizeFromContents(style.CT_MenuItem, opt, size, self).height()
if not count:
self.setMaximumHeight(self._maximumHeight)
else:
fw = style.pixelMetric(style.PM_MenuPanelWidth, None, self)
vmargin = style.pixelMetric(style.PM_MenuHMargin, opt, self)
scrollHeight = self.scrollHeight(style)
self.setMaximumHeight(
self.defaultItemHeight * count + (fw + vmargin + scrollHeight) * 2)
self.dirty = True
def scrollHeight(self, style):
return style.pixelMetric(style.PM_MenuScrollerHeight, None, self) * 2
def isScrollable(self):
return self.height() < super().sizeHint().height()
def checkScroll(self):
pos = self.mapFromGlobal(QtGui.QCursor.pos())
delta = max(2, int(self.defaultItemHeight * .25))
if pos in self.scrollUpRect:
delta *= -1
elif pos not in self.scrollDownRect:
return
if self.scrollBy(delta):
self.scrollTimer.start(self.scrollTimer.property('defaultInterval'))
def offset(self):
if self.isScrollable():
return self.deltaY - self.scrollHeight(self.style())
return 0
def translatedActionGeometry(self, action):
return self.actionRects[self.actions().index(action)]
def ensureVisible(self, action):
style = self.style()
fw = style.pixelMetric(style.PM_MenuPanelWidth, None, self)
hmargin = style.pixelMetric(style.PM_MenuHMargin, None, self)
vmargin = style.pixelMetric(style.PM_MenuVMargin, None, self)
scrollHeight = self.scrollHeight(style)
extent = fw + hmargin + vmargin + scrollHeight
r = self.rect().adjusted(0, extent, 0, -extent)
geo = self.translatedActionGeometry(action)
if geo.top() < r.top():
self.scrollBy(-(r.top() - geo.top()))
elif geo.bottom() > r.bottom():
self.scrollBy(geo.bottom() - r.bottom())
def scrollBy(self, step):
if step < 0:
newDelta = max(0, self.deltaY + step)
if newDelta == self.deltaY:
return False
elif step > 0:
newDelta = self.deltaY + step
style = self.style()
scrollHeight = self.scrollHeight(style)
bottom = self.height() - scrollHeight
for lastAction in reversed(self.actions()):
if lastAction.isVisible():
break
lastBottom = self.actionGeometry(lastAction).bottom() - newDelta + scrollHeight
if lastBottom < bottom:
newDelta -= bottom - lastBottom
if newDelta == self.deltaY:
return False
self.deltaY = newDelta
self.dirty = True
self.update()
return True
def actionAt(self, pos):
for action, rect in self.iterActionRects():
if pos in rect:
return action
# class methods reimplementation
def sizeHint(self):
hint = super().sizeHint()
if hint.height() > self.maximumHeight():
hint.setHeight(self.maximumHeight())
return hint
def eventFilter(self, source, event):
if event.type() == event.Show:
if self.isScrollable() and self.deltaY:
action = source.menuAction()
self.ensureVisible(action)
rect = self.translatedActionGeometry(action)
delta = rect.topLeft() - self.actionGeometry(action).topLeft()
source.move(source.pos() + delta)
return False
return super().eventFilter(source, event)
def event(self, event):
if not self.isScrollable():
return super().event(event)
if event.type() == event.KeyPress and event.key() in (QtCore.Qt.Key_Up, QtCore.Qt.Key_Down):
res = super().event(event)
action = self.activeAction()
if action:
self.ensureVisible(action)
self.update()
return res
elif event.type() in (event.MouseButtonPress, event.MouseButtonDblClick):
pos = event.pos()
if pos in self.scrollUpRect or pos in self.scrollDownRect:
if event.button() == QtCore.Qt.LeftButton:
step = max(2, int(self.defaultItemHeight * .25))
if pos in self.scrollUpRect:
step *= -1
self.scrollBy(step)
self.scrollTimer.start(200)
self.ignoreAutoScroll = True
return True
elif event.type() == event.MouseButtonRelease:
pos = event.pos()
self.scrollTimer.stop()
if not (pos in self.scrollUpRect or pos in self.scrollDownRect):
action = self.actionAt(event.pos())
if action:
action.trigger()
self.close()
return True
return super().event(event)
def timerEvent(self, event):
if not self.isScrollable():
# ignore internal timer event for reopening popups
super().timerEvent(event)
def mouseMoveEvent(self, event):
if not self.isScrollable():
super().mouseMoveEvent(event)
return
pos = event.pos()
if pos.y() < self.scrollUpRect.bottom() or pos.y() > self.scrollDownRect.top():
if not self.ignoreAutoScroll and not self.scrollTimer.isActive():
self.scrollTimer.start(200)
return
self.ignoreAutoScroll = False
oldAction = self.activeAction()
if not pos in self.rect():
action = None
else:
y = event.y()
for action, rect in self.iterActionRects():
if rect.y() <= y <= rect.y() + rect.height():
break
else:
action = None
self.setActiveAction(action)
if action and not action.isSeparator():
def ensureVisible():
self.delayTimer.timeout.disconnect()
self.ensureVisible(action)
try:
self.delayTimer.disconnect()
except:
pass
self.delayTimer.timeout.connect(ensureVisible)
self.delayTimer.start(150)
elif oldAction and oldAction.menu() and oldAction.menu().isVisible():
def closeMenu():
self.delayTimer.timeout.disconnect()
oldAction.menu().hide()
self.delayTimer.timeout.connect(closeMenu)
self.delayTimer.start(50)
self.update()
def wheelEvent(self, event):
if not self.isScrollable():
return
self.delayTimer.stop()
if event.angleDelta().y() < 0:
self.scrollBy(self.defaultItemHeight)
else:
self.scrollBy(-self.defaultItemHeight)
def showEvent(self, event):
if self.isScrollable():
self.deltaY = 0
self.dirty = True
for action in self.actions():
if action.menu():
action.menu().installEventFilter(self)
self.ignoreAutoScroll = False
super().showEvent(event)
def hideEvent(self, event):
for action in self.actions():
if action.menu():
action.menu().removeEventFilter(self)
super().hideEvent(event)
def resizeEvent(self, event):
super().resizeEvent(event)
style = self.style()
l, t, r, b = self.getContentsMargins()
fw = style.pixelMetric(style.PM_MenuPanelWidth, None, self)
hmargin = style.pixelMetric(style.PM_MenuHMargin, None, self)
vmargin = style.pixelMetric(style.PM_MenuVMargin, None, self)
leftMargin = fw + hmargin + l
topMargin = fw + vmargin + t
bottomMargin = fw + vmargin + b
contentWidth = self.width() - (fw + hmargin) * 2 - l - r
scrollHeight = self.scrollHeight(style)
self.scrollUpRect = QtCore.QRect(leftMargin, topMargin, contentWidth, scrollHeight)
self.scrollDownRect = QtCore.QRect(leftMargin, self.height() - scrollHeight - bottomMargin,
contentWidth, scrollHeight)
def paintEvent(self, event):
if not self.isScrollable():
super().paintEvent(event)
return
style = self.style()
qp = QtGui.QPainter(self)
rect = self.rect()
emptyArea = QtGui.QRegion(rect)
menuOpt = QtWidgets.QStyleOptionMenuItem()
menuOpt.initFrom(self)
menuOpt.state = style.State_None
menuOpt.maxIconWidth = 0
menuOpt.tabWidth = 0
style.drawPrimitive(style.PE_PanelMenu, menuOpt, qp, self)
fw = style.pixelMetric(style.PM_MenuPanelWidth, None, self)
topEdge = self.scrollUpRect.bottom()
bottomEdge = self.scrollDownRect.top()
offset = self.offset()
qp.save()
qp.translate(0, -offset)
# offset translation is required in order to allow correct fade animations
for action, actionRect in self.iterActionRects():
actionRect = self.translatedActionGeometry(action)
if actionRect.bottom() < topEdge:
continue
if actionRect.top() > bottomEdge:
continue
visible = QtCore.QRect(actionRect)
if actionRect.bottom() > bottomEdge:
visible.setBottom(bottomEdge)
elif actionRect.top() < topEdge:
visible.setTop(topEdge)
visible = QtGui.QRegion(visible.translated(0, offset))
qp.setClipRegion(visible)
emptyArea -= visible.translated(0, -offset)
opt = QtWidgets.QStyleOptionMenuItem()
self.initStyleOption(opt, action)
opt.rect = actionRect.translated(0, offset)
style.drawControl(style.CE_MenuItem, opt, qp, self)
qp.restore()
cursor = self.mapFromGlobal(QtGui.QCursor.pos())
upData = (
False, self.deltaY > 0, self.scrollUpRect
)
downData = (
True, actionRect.bottom() - 2 > bottomEdge, self.scrollDownRect
)
for isDown, enabled, scrollRect in upData, downData:
qp.setClipRect(scrollRect)
scrollOpt = QtWidgets.QStyleOptionMenuItem()
scrollOpt.initFrom(self)
scrollOpt.state = style.State_None
scrollOpt.checkType = scrollOpt.NotCheckable
scrollOpt.maxIconWidth = scrollOpt.tabWidth = 0
scrollOpt.rect = scrollRect
scrollOpt.menuItemType = scrollOpt.Scroller
if enabled:
if cursor in scrollRect:
frame = QtWidgets.QStyleOptionMenuItem()
frame.initFrom(self)
frame.rect = scrollRect
frame.state |= style.State_Selected | style.State_Enabled
style.drawControl(style.CE_MenuItem, frame, qp, self)
scrollOpt.state |= style.State_Enabled
scrollOpt.palette.setCurrentColorGroup(QtGui.QPalette.Active)
else:
scrollOpt.palette.setCurrentColorGroup(QtGui.QPalette.Disabled)
if isDown:
scrollOpt.state |= style.State_DownArrow
style.drawControl(style.CE_MenuScroller, scrollOpt, qp, self)
if fw:
borderReg = QtGui.QRegion()
borderReg |= QtGui.QRegion(QtCore.QRect(0, 0, fw, self.height()))
borderReg |= QtGui.QRegion(QtCore.QRect(self.width() - fw, 0, fw, self.height()))
borderReg |= QtGui.QRegion(QtCore.QRect(0, 0, self.width(), fw))
borderReg |= QtGui.QRegion(QtCore.QRect(0, self.height() - fw, self.width(), fw))
qp.setClipRegion(borderReg)
emptyArea -= borderReg
frame = QtWidgets.QStyleOptionFrame()
frame.rect = rect
frame.palette = self.palette()
frame.state = QtWidgets.QStyle.State_None
frame.lineWidth = style.pixelMetric(style.PM_MenuPanelWidth)
frame.midLineWidth = 0
style.drawPrimitive(style.PE_FrameMenu, frame, qp, self)
qp.setClipRegion(emptyArea)
menuOpt.state = style.State_None
menuOpt.menuItemType = menuOpt.EmptyArea
menuOpt.checkType = menuOpt.NotCheckable
menuOpt.rect = menuOpt.menuRect = rect
style.drawControl(style.CE_MenuEmptyArea, menuOpt, qp, self)
class Test(QtWidgets.QWidget):
def __init__(self):
super().__init__()
self.menu = ScrollableMenu(maxItemCount=5)
self.menu.addAction('test action')
for i in range(10):
self.menu.addAction('Action {}'.format(i + 1))
if i & 1:
self.menu.addSeparator()
subMenu = self.menu.addMenu('very long sub menu')
subMenu.addAction('goodbye')
self.menu.triggered.connect(self.menuTriggered)
def menuTriggered(self, action):
print(action.text())
def contextMenuEvent(self, event):
self.menu.exec_(event.globalPos())
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication(sys.argv)
test = Test()
test.show()
sys.exit(app.exec_())
For this, you will have to inherit the QMenu and override the wheelEvent.
Here is an example, which you may improve.
import sys
from PyQt5 import QtWidgets, QtCore, QtGui
class CustomMenu(QtWidgets.QMenu):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.setStyleSheet('QMenu{background: gray;} QMenu::item:selected {background: rgb(58, 145, 232);}')
self.setFixedHeight(100)
self.visible_lst = []
self.index = -1
self.visibleCount = None
self.maxHeightOfAction = 0
# -------- Remove Below code if you don't want the arrow ---------------#
self.topArrow = None
self.bottomArrow = None
self.painter = QtGui.QPainter()
# ---------------------------------------- #
def actionEvent(self, event):
if event.type() == QtCore.QEvent.ActionAdded:
if self.maxHeightOfAction < self.actionGeometry(self.actions()[-1]).height():
self.maxHeightOfAction = self.actionGeometry(self.actions()[-1]).height()
self.actions()[-1].setVisible(False)
self.updateActions()
if event.type() == QtCore.QEvent.ActionRemoved:
super(CustomMenu, self).actionEvent(event)
if self.index == len(self.actions()):
self.index -= 1
if event.action() in self.visible_lst:
self.visible_lst.remove(event.action())
self.removed()
super(CustomMenu, self).actionEvent(event)
def updateActions(self):
if self.actions():
if self.findVisibleCount() > len(self.actions()) and self.index == -1:
self.visible_lst = self.actions()
self.updateVisible()
elif self.findVisibleCount() < len(self.actions()) and self.index == -1:
self.index += 1
self.visible_lst = self.actions()[0: self.findVisibleCount()]
self.updateVisible()
self.setActiveAction(self.visible_lst[0])
def removed(self):
if len(self.actions()) > self.findVisibleCount():
if self.index < len(self.actions())-2:
index = self.findIndex(self.visible_lst, self.activeAction())
self.visible_lst.append(self.actions()[self.index + (index-self.findVisibleCount())-1])
elif self.index == len(self.actions())-1:
self.visible_lst.insert(0, self.actions()[-self.findVisibleCount()-1])
self.updateVisible()
def findVisibleCount(self): # finds how many QActions will be visible
visibleWidgets = 0
if self.actions():
try:
visibleWidgets = self.height()//self.maxHeightOfAction
except ZeroDivisionError:
pass
return visibleWidgets
def mousePressEvent(self, event) -> None:
if self.topArrow.containsPoint(event.pos(), QtCore.Qt.OddEvenFill) and self.index>0:
self.scrollUp()
elif self.bottomArrow.containsPoint(event.pos(), QtCore.Qt.OddEvenFill) and self.index < len(self.actions()) -1:
self.scrollDown()
else:
super(CustomMenu, self).mousePressEvent(event)
def keyPressEvent(self, event):
if self.actions():
if self.activeAction() is None:
self.setActiveAction(self.actions()[self.index])
if event.key() == QtCore.Qt.Key_Up:
self.scrollUp()
elif event.key() == QtCore.Qt.Key_Down:
self.scrollDown()
elif event.key() == QtCore.Qt.Key_Return:
super(CustomMenu, self).keyPressEvent(event)
def wheelEvent(self, event):
if self.actions():
if self.activeAction() is None:
self.setActiveAction(self.actions()[self.index])
delta = event.angleDelta().y()
if delta < 0: # scroll down
self.scrollDown()
elif delta > 0: # scroll up
self.scrollUp()
def scrollDown(self):
if self.index < len(self.actions())-1:
self.index = self.findIndex(self.actions(), self.activeAction()) + 1
try:
self.setActiveAction(self.actions()[self.index])
if self.activeAction() not in self.visible_lst and len(self.actions()) > self.findVisibleCount():
self.visible_lst[0].setVisible(False)
self.visible_lst.pop(0)
self.visible_lst.append(self.actions()[self.index])
self.visible_lst[-1].setVisible(True)
except IndexError:
pass
def scrollUp(self):
if self.findIndex(self.actions(), self.activeAction()) > 0:
self.index = self.findIndex(self.actions(), self.activeAction()) - 1
try:
self.setActiveAction(self.actions()[self.index])
if self.activeAction() not in self.visible_lst and len(self.actions()) > self.findVisibleCount():
self.visible_lst[-1].setVisible(False)
self.visible_lst.pop()
self.visible_lst.insert(0, self.actions()[self.index])
self.visible_lst[0].setVisible(True)
except IndexError:
pass
def updateVisible(self):
for item in self.visible_lst:
if not item.isVisible():
item.setVisible(True)
def findIndex(self, lst, element):
for index, item in enumerate(lst):
if item == element:
return index
return -1
def paintEvent(self, event): # remove this if you don't want the arrow
super(CustomMenu, self).paintEvent(event)
height = int(self.height())
width = self.width()//2
topPoints = [QtCore.QPoint(width-5, 7), QtCore.QPoint(width, 2), QtCore.QPoint(width+5, 7)]
bottomPoints = [QtCore.QPoint(width-5, height-7), QtCore.QPoint(width, height-2), QtCore.QPoint(width+5, height-7)]
self.topArrow = QtGui.QPolygon(topPoints)
self.bottomArrow = QtGui.QPolygon(bottomPoints)
self.painter.begin(self)
self.painter.setBrush(QtGui.QBrush(QtCore.Qt.white))
self.painter.setPen(QtCore.Qt.white)
if len(self.actions()) > self.findVisibleCount():
if self.index>0:
self.painter.drawPolygon(self.topArrow)
if self.index < len(self.actions()) -1:
self.painter.drawPolygon(self.bottomArrow)
self.painter.end()
class ExampleWindow(QtWidgets.QWidget):
def contextMenuEvent(self, event) -> None:
menu = CustomMenu()
menu.addAction('Hello0')
menu.addAction('Hello1')
menu.addAction('Hello2')
menu.addAction('Hello3')
menu.addAction('Hello4')
menu.addAction('Hello5')
menu.addAction('Hello6')
menu.addAction('Hello7')
menu.addAction('Hello8')
menu.exec_(QtGui.QCursor.pos())
def main():
app = QtWidgets.QApplication(sys.argv)
window = ExampleWindow()
window.setWindowTitle('PyQt5 App')
window.show()
app.exec_()
if __name__ == '__main__':
main()
Explanation of the above code:
Store all the visible actions in a list say visible_lst.
scrolling down:
increment the index when scrolling down
Use self.visible_lst[0].setVisible(False) which will make that action invisible and then pop the list from the front.
Append the next action to the visible_lst using self.actions()[self.index]
scrolling up:
decrement the index when scrolling up
use self.visible_lst[-1].setVisible(False) which will make the last item in the list hidden and pop the last element from the list
Insert the previous element to the 0th index of visible_lst and make it visible using self.actions()[index].setVisible(True)
Output of the scrolling context menu code:
Readers, if you have suggestions or queries leave a comment.
When using scikit CollectionViewer (simple image browser) I'd like pressing arrow keys not to trigger going to prev/next image after slider got focus. I used eventFilter for that
from skimage.viewer import ImageViewer
from skimage.viewer.qt import Qt
from skimage.viewer.widgets import Slider
class SilentViewer(ImageViewer): #CollectionViewer with some modifications
def __init__(self, image_collection, update_on='move', **kwargs):
self.image_collection = image_collection
self.index = 0
self.num_images = len(self.image_collection)
first_image = image_collection[0]
super(SilentViewer, self).__init__(first_image)
slider_kws = dict(value=0, low=0, high=self.num_images - 1)
slider_kws['update_on'] = update_on
slider_kws['callback'] = self.update_index
slider_kws['value_type'] = 'int'
self.slider = Slider('frame', **slider_kws)
self.layout.addWidget(self.slider)
self.installEventFilter(self) #Modification to CollectionViewer №1
def update_index(self, name, index):
index = int(round(index))
if index == self.index:
return
index = max(index, 0)
index = min(index, self.num_images - 1)
self.index = index
self.slider.val = index
self.update_image(self.image_collection[index])
def eventFilter(self,obj,evt): #Modification to CollectionViewer №2
try:
print(evt.type(), evt.key(), evt.text())
if (evt.key() == Qt.Key_Left or
evt.key() == Qt.Key_Right or
evt.key() == Qt.Key_Up or
evt.key() == Qt.Key_Down):
print("Ignored arrow button")
return True
else:
return False
except:
print("Smth went wrong")
return False
#for testing reasons
from skimage import data
from skimage.transform import pyramid_gaussian
img = data.coins()
img_pyr = pyramid_gaussian(img, downscale=2, multichannel=False)
img_collection = tuple(img_pyr)
viewer = SilentViewer(img_collection)
viewer.show()
event filter seems to be working: key presses and other events trigger console output. However, arrow keys trigger image change. If I change to update_on='release' (see init), arrow keys do not trigger the image change, but make slider position change.
Could you please tell how can I make the arrow presses to be full consumed by the filter?
Analyzing the Slider source code, it can be seen that it is a container, that is, a widget that has other widgets (QLabel, QSlider and QLineEdit), so the filter must be installed on the internal QSlider and not on the container.
from skimage.viewer import ImageViewer
from skimage.viewer.qt import QtCore
from skimage.viewer.widgets import Slider
class SilentViewer(ImageViewer): # CollectionViewer with some modifications
def __init__(self, image_collection, update_on="move", **kwargs):
self.image_collection = image_collection
self.index = 0
self.num_images = len(self.image_collection)
print(self.num_images)
first_image = image_collection[0]
super(SilentViewer, self).__init__(first_image)
slider_kws = dict(value=0, low=0, high=self.num_images - 1)
slider_kws["update_on"] = update_on
slider_kws["callback"] = self.update_index
slider_kws["value_type"] = "int"
self.slider = Slider("frame", **slider_kws)
self.layout.addWidget(self.slider)
self.slider.slider.installEventFilter(self)
def update_index(self, name, index):
if index == self.index:
return
index = max(index, 0)
index = min(index, self.num_images - 1)
self.index = index
self.update_image(self.image_collection[index])
def eventFilter(self, obj, evt):
if obj is self.slider.slider and evt.type() == QtCore.QEvent.KeyPress:
if evt.key() in (
QtCore.Qt.Key_Left,
QtCore.Qt.Key_Right,
QtCore.Qt.Key_Up,
QtCore.Qt.Key_Down,
):
return True
return super(SilentViewer, self).eventFilter(obj, evt)
def main():
# for testing reasons
from skimage import data
from skimage.transform import pyramid_gaussian
img = data.coins()
img_pyr = pyramid_gaussian(img, downscale=2, multichannel=False)
img_collection = tuple(img_pyr)
viewer = SilentViewer(img_collection)
viewer.show()
if __name__ == "__main__":
main()
I'm working on a PyQt5 GUI, so far, I've just had experience with python scripts and did not delve into creating user interfaces.
The GUI will have to be used on different screens (maybe also some old 4:3 ratio screens) and will need to look nice in different sizes.
Now, my approach to make my life easier was to enforce a fixed aspect ratio of the window and resize the different elements according to window size.
from PyQt5 import QtCore, QtGui, QtWidgets
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent= None):
super().__init__(parent)
self.form_widget = FormWidget(self)
self.setCentralWidget(self.form_widget)
self.resize(200, 400)
self.sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred)
self.sizePolicy.setHeightForWidth(True)
self.setSizePolicy(self.sizePolicy)
def heightForWidth(self, width):
return width * 2
class FormWidget(QtWidgets.QWidget):
def __init__(self, parent):
super().__init__(parent)
def resizeEvent(self, event):
f = self.font()
temp = event.size().height()
f.setPixelSize(temp / 16)
self.setFont(f)
return super().resizeEvent(event)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
Resizing the elements according to window size works fine, but window aspect ratio is not kept at all.
I copied this approach with heightForWidth from old PyQt4 threads. Doesn't this approach work anymore in PyQt5? Am I missing something?
If I understood your question, you should try using a layout inside the main window.
I did this:
from PyQt5 import QtCore, QtGui, QtWidgets
class MainWindow(QtWidgets.QMainWindow):
def __init__(self, parent= None):
super().__init__(parent)
self.central_widget = QtWidgets.QWidget()
self.central_layout = QtWidgets.QVBoxLayout()
self.setCentralWidget(self.central_widget)
self.central_widget.setLayout(self.central_layout)
# Lets create some widgets inside
self.label = QtWidgets.QLabel()
self.list_view = QtWidgets.QListView()
self.push_button = QtWidgets.QPushButton()
self.label.setText('Hi, this is a label. And the next one is a List View :')
self.push_button.setText('Push Button Here')
# Lets add the widgets
self.central_layout.addWidget(self.label)
self.central_layout.addWidget(self.list_view)
self.central_layout.addWidget(self.push_button)
if __name__ == "__main__":
import sys
app = QtWidgets.QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
If you resize the window, the widgets inside it get resized.
First, answered by Marc and codeling in this question, heightForWidth is only supported for QGraphicsLayout's subclasses.
Second, how to make a fixed aspect ratio window (or top-level widget) in qt (or pyqt) is a question that have been asked for years. However, as far as I know, there is no standard way of doing so, and it is something surprisingly hard to achieve. In short, my way of doing this is use Qt.FramelessWindowHint to create a frameless window without system move and resize function, and implement custom move and resize.
Explain important mechanism:
move:
In mousePressEvent, keep the place where we last clicked on the widget(the draggable area).
In mouseMoveEvent, calculate the distance between the last clicked point and the current mouse location. Move the window according to this distance.
resize:
Find the increase or decrease step size of width and height by dividing the minimum width and height of the window by their highest common factor.
Use the step size to increase or decrease the window size to keep the aspect ratio.
A screenshot to show that it can resize according to the aspect ratio.
The following code should works with both PyQt5 and Pyside2.
from PyQt5.QtCore import Qt, QRect, QPoint, QEvent
from PyQt5.QtWidgets import (QLabel, QMainWindow, QApplication, QSizePolicy,
QVBoxLayout, QWidget, QHBoxLayout, QPushButton)
from enum import Enum
class MainWindow(QMainWindow):
def __init__(self, parent=None):
super(MainWindow, self).__init__(parent)
self.setWindowFlags(Qt.FramelessWindowHint)
self.createCostumTitleBar()
self.setContentsMargins(0, 0, 0, 0)
self.central = QWidget()
self.central.setStyleSheet("background-color: #f8ecdf")
self.centralLayout = QVBoxLayout()
self.central.setLayout(self.centralLayout)
self.centralLayout.addWidget(
self.costumsystemmenu, alignment=Qt.AlignTop)
self.centralLayout.setContentsMargins(0, 0, 0, 0)
self.setCentralWidget(self.central)
# Set the minimum size to avoid window being resized too small.
self.setMinimumSize(300, 400)
self.minheight = self.minimumHeight()
self.minwidth = self.minimumWidth()
self.resize(300, 400)
# make sure your minium size have the same aspect ratio as the step.
self.stepY = 4
self.stepX = 3
# install the event filter on this window.
self.installEventFilter(self)
self.grabarea.installEventFilter(self)
self.cursorpos = CursorPos.DEFAULT
self.iswindowpress = False
def createCostumTitleBar(self):
self.costumsystemmenu = QWidget()
self.costumsystemmenu.setStyleSheet("background-color: #ccc")
self.costumsystemmenu.setContentsMargins(0, 0, 0, 0)
self.costumsystemmenu.setMinimumHeight(30)
self.grabarea = QLabel("")
self.grabarea.setStyleSheet("background-color: #ccc")
self.grabarea.setSizePolicy(
QSizePolicy.Expanding, QSizePolicy.Preferred)
titlebarlayout = QHBoxLayout()
titlebarlayout.setContentsMargins(11, 11, 11, 11)
titlebarlayout.setSpacing(0)
self.closeButton = QPushButton("X")
self.closeButton.setSizePolicy(
QSizePolicy.Minimum, QSizePolicy.Preferred)
self.closeButton.clicked.connect(self.close)
self.costumsystemmenu.setLayout(titlebarlayout)
titlebarlayout.addWidget(self.grabarea)
titlebarlayout.addWidget(self.closeButton, alignment=Qt.AlignRight)
self.istitlebarpress = False
def eventFilter(self, object, event):
# The eventFilter() function must return true if the event
# should be filtered, (i.e. stopped); otherwise it must return false.
# https://doc.qt.io/qt-5/qobject.html#eventFilter
# check if the object is the mainwindow.
if object == self:
if event.type() == QEvent.HoverMove:
if not self.iswindowpress:
self.setCursorShape(event)
return True
elif event.type() == QEvent.MouseButtonPress:
self.iswindowpress = True
# Get the position of the cursor and map to the global coordinate of the widget.
self.globalpos = self.mapToGlobal(event.pos())
self.origingeometry = self.geometry()
return True
elif event.type() == QEvent.MouseButtonRelease:
self.iswindowpress = False
return True
elif event.type() == QEvent.MouseMove:
if self.cursorpos != CursorPos.DEFAULT and self.iswindowpress:
self.resizing(self.globalpos, event,
self.origingeometry, self.cursorpos)
return True
else:
return False
elif object == self.grabarea:
if event.type() == QEvent.MouseButtonPress:
if event.button() == Qt.LeftButton and self.iswindowpress == False:
self.oldpos = event.globalPos()
self.oldwindowpos = self.pos()
self.istitlebarpress = True
return True
elif event.type() == QEvent.MouseButtonRelease:
self.istitlebarpress = False
return True
elif event.type() == QEvent.MouseMove:
if (self.istitlebarpress):
distance = event.globalPos()-self.oldpos
newwindowpos = self.oldwindowpos + distance
self.move(newwindowpos)
return True
else:
return False
else:
return False
# Change the cursor shape when the cursor is over different part of the window.
def setCursorShape(self, event, handlersize=11):
rect = self.rect()
topLeft = rect.topLeft()
topRight = rect.topRight()
bottomLeft = rect.bottomLeft()
bottomRight = rect.bottomRight()
# get the position of the cursor
pos = event.pos()
# make the resize handle include some space outside the window,
# can avoid user move too fast and loss the handle.
# top handle
if pos in QRect(QPoint(topLeft.x()+handlersize, topLeft.y()-2*handlersize),
QPoint(topRight.x()-handlersize, topRight.y()+handlersize)):
self.setCursor(Qt.SizeVerCursor)
self.cursorpos = CursorPos.TOP
# bottom handle
elif pos in QRect(QPoint(bottomLeft.x()+handlersize, bottomLeft.y()-handlersize),
QPoint(bottomRight.x()-handlersize, bottomRight.y()+2*handlersize)):
self.setCursor(Qt.SizeVerCursor)
self.cursorpos = CursorPos.BOTTOM
# right handle
elif pos in QRect(QPoint(topRight.x()-handlersize, topRight.y()+handlersize),
QPoint(bottomRight.x()+2*handlersize, bottomRight.y()-handlersize)):
self.setCursor(Qt.SizeHorCursor)
self.cursorpos = CursorPos.RIGHT
# left handle
elif pos in QRect(QPoint(topLeft.x()-2*handlersize, topLeft.y()+handlersize),
QPoint(bottomLeft.x()+handlersize, bottomLeft.y()-handlersize)):
self.setCursor(Qt.SizeHorCursor)
self.cursorpos = CursorPos.LEFT
# topRight handle
elif pos in QRect(QPoint(topRight.x()-handlersize, topRight.y()-2*handlersize),
QPoint(topRight.x()+2*handlersize, topRight.y()+handlersize)):
self.setCursor(Qt.SizeBDiagCursor)
self.cursorpos = CursorPos.TOPRIGHT
# topLeft handle
elif pos in QRect(QPoint(topLeft.x()-2*handlersize, topLeft.y()-2*handlersize),
QPoint(topLeft.x()+handlersize, topLeft.y()+handlersize)):
self.setCursor(Qt.SizeFDiagCursor)
self.cursorpos = CursorPos.TOPLEFT
# bottomRight handle
elif pos in QRect(QPoint(bottomRight.x()-handlersize, bottomRight.y()-handlersize),
QPoint(bottomRight.x()+2*handlersize, bottomRight.y()+2*handlersize)):
self.setCursor(Qt.SizeFDiagCursor)
self.cursorpos = CursorPos.BOTTOMRIGHT
# bottomLeft handle
elif pos in QRect(QPoint(bottomLeft.x()-2*handlersize, bottomLeft.y()-handlersize),
QPoint(bottomLeft.x()+handlersize, bottomLeft.y()+2*handlersize)):
self.setCursor(Qt.SizeBDiagCursor)
self.cursorpos = CursorPos.BOTTOMLEFT
# Default is the arrow cursor.
else:
self.setCursor(Qt.ArrowCursor)
self.cursorpos = CursorPos.DEFAULT
def resizing(self, originpos, event, geo, cursorpos):
newpos = self.mapToGlobal(event.pos())
# find the distance between new and old cursor position.
dist = newpos - originpos
# calculate the steps to grow or srink.
if cursorpos in [CursorPos.TOP, CursorPos.BOTTOM,
CursorPos.TOPRIGHT,
CursorPos.BOTTOMLEFT, CursorPos.BOTTOMRIGHT]:
steps = dist.y()//self.stepY
elif cursorpos in [CursorPos.LEFT, CursorPos.TOPLEFT, CursorPos.RIGHT]:
steps = dist.x()//self.stepX
# if the distance moved is too stort, grow or srink by 1 step.
if steps == 0:
steps = -1 if dist.y() < 0 or dist.x() < 0 else 1
oldwidth = geo.width()
oldheight = geo.height()
oldX = geo.x()
oldY = geo.y()
if cursorpos in [CursorPos.TOP, CursorPos.TOPRIGHT]:
width = oldwidth - steps * self.stepX
height = oldheight - steps * self.stepY
newX = oldX
newY = oldY + (steps * self.stepY)
# check if the new size is within the size limit.
if height >= self.minheight and width >= self.minwidth:
self.setGeometry(newX, newY, width, height)
elif cursorpos in [CursorPos.BOTTOM, CursorPos.RIGHT, CursorPos.BOTTOMRIGHT]:
width = oldwidth + steps * self.stepX
height = oldheight + steps * self.stepY
self.resize(width, height)
elif cursorpos in [CursorPos.LEFT, CursorPos.BOTTOMLEFT]:
width = oldwidth - steps * self.stepX
height = oldheight - steps * self.stepY
newX = oldX + steps * self.stepX
newY = oldY
# check if the new size is within the size limit.
if height >= self.minheight and width >= self.minwidth:
self.setGeometry(newX, newY, width, height)
elif cursorpos == CursorPos.TOPLEFT:
width = oldwidth - steps * self.stepX
height = oldheight - steps * self.stepY
newX = oldX + steps * self.stepX
newY = oldY + steps * self.stepY
# check if the new size is within the size limit.
if height >= self.minheight and width >= self.minwidth:
self.setGeometry(newX, newY, width, height)
else:
pass
# cursor position
class CursorPos(Enum):
TOP = 1
BOTTOM = 2
RIGHT = 3
LEFT = 4
TOPRIGHT = 5
TOPLEFT = 6
BOTTOMRIGHT = 7
BOTTOMLEFT = 8
DEFAULT = 9
if __name__ == "__main__":
import sys
app = QApplication(sys.argv)
w = MainWindow()
w.show()
sys.exit(app.exec_())
Finally, I'd like to give special thanks to the authors and editors of this question, GLHF, DRPK, Elad Joseph, and SimoN SavioR. Without their contribution to the community, it wouldn't be possible to come up with this answer.
I have a set of images that I want to be able to scroll through. The below code works for me on Mac, such that I can scroll on the visible widget. Printing "count" results in a visible rising or lowering number in the python output. However, I wish to use "count" to update the image and I am kinda stuck on how.
The main question is: How can I use the constantly varying variable "count" from my mousewheel function, and use it to update the image shown in the DCMViewer? I expect some kind of signal to work, but haven't got it working and I'm stuck. Any help is much appreciated.
class DCMViewer(QGraphicsView):
def __init__(self):
super(DCMViewer, self).__init__()
# Create a QGraphicsScene
self.scene = QGraphicsScene()
# Provide image in normalized double format
image = np.double(dcmIm)
image *= 255.0 / image.max()
image = QPixmap.fromImage(qimage2ndarray.array2qimage(image))
image = QGraphicsPixmapItem(image)
# Add the image to the QGraphicsScene window
self.scene.addItem(image)
# Initiate the scene
self.setScene(self.scene)
# Create a mousewheelevent to scroll through images
def wheelEvent(self, QWheelEvent):
super(DCMViewer, self).wheelEvent(QWheelEvent)
global count
wheelcounter = QWheelEvent.angleDelta()
if wheelcounter.y() / 120 == -1:
count += 1
if count >= 21:
count = 21
elif wheelcounter.y() / 120 == 1:
count -= 1
if count <= 0:
count = 0
class Mainwidget(QMainWindow):
def __init__(self):
super(Mainwidget, self).__init__()
# self.initUI()
image_viewer = DCMViewer()
self.setCentralWidget(image_viewer)
self.showFullScreen()
if __name__ == '__main__':
app = QApplication(sys.argv)
win = Mainwidget()
win.showFullScreen()
sys.exit(app.exec_())
To be able to make an item visible, the first task we must do is to identify it, for that we could establish an id for each item. We use the setData() and data() method, then we use the fitInView() method to scale the window to the item, this does not make the item totally visible because if there is another item on it we will get an unwanted output, for that we use setZValue(), the higher the value of Z will be on top of another item, your code will not provide an example that can be tested correctly so create my own example:
import sys
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
class DCMViewer(QGraphicsView):
def __init__(self):
super(DCMViewer, self).__init__()
self.currentId = 0
self.maxId = 0
self.setScene(QGraphicsScene())
directory = QStandardPaths.writableLocation(QStandardPaths.PicturesLocation)
it = QDirIterator(directory, QDir.Files)
while it.hasNext():
pixmap = QPixmap(it.next())
item = QGraphicsPixmapItem(pixmap)
item.setData(0, self.maxId)
self.scene().addItem(item)
self.maxId += 1
def scrollToItem(self, id):
for item in self.scene().items():
if item.data(0) == id:
item.setZValue(1)
self.fitInView(item)
else:
item.setZValue(0)
def wheelEvent(self, event):
wheelcounter = event.angleDelta()
if wheelcounter.y() / 120 == -1:
self.currentId += 1
if self.currentId > self.maxId:
self.currentId = self.maxId -1
elif wheelcounter.y() / 120 == 1:
self.currentId -= 1
if self.currentId <= 0:
self.currentId = 0
self.scrollToItem(self.currentId)
class Mainwidget(QMainWindow):
def __init__(self):
super(Mainwidget, self).__init__()
image_viewer = DCMViewer()
self.setCentralWidget(image_viewer)
self.showFullScreen()
image_viewer.scrollToItem(0)
if __name__ == '__main__':
app = QApplication(sys.argv)
win = Mainwidget()
sys.exit(app.exec_())
So I have a main window. I use setCentralWidget to set it to a class I made called mainWindowWidget to handle the UI.
I am now trying to add a graphics view widget I made to that but cannot seem to get anything to display. If I set the graphics view as the central widget I can see it and all my behaviour is working.
Do I need to do anything to get the graphics view to display within another widget?
Below are the sections of code I think are relevant to the question followed by the entire application. I am really new to PyQt and any guidance would be appreciated.
class mainWindowWidget(QtGui.QWidget):
grid = None
scene = None
def __init__(self):
self.initScene()
QtGui.QWidget.__init__(self)
def initScene(self):
self.grid = QtGui.QGridLayout()
'''Node Interface'''
self.scene = Scene(0, 0, 1280, 720, self)
self.view = QtGui.QGraphicsView()
self.view.setScene(self.scene)
self.grid.addWidget(self.view)
'''AttributeWindow'''
class MainWindowUi(QtGui.QMainWindow):
def __init__(self):
mainDataGraber = ind.dataGraber()
QtGui.QMainWindow.__init__(self)
self.setWindowTitle('RIS RIB Generator')
mainwindowwidget = mainWindowWidget()
self.setCentralWidget(mainwindowwidget)
This is the main GUI file for the application
#!/usr/bin/python
# -*- coding: utf-8 -*-
"""
This is the base py file for the GUI
Todo list
-----------------
- Pop up menu for adding new Nodes
- node connectivity
- create data structure for storing
"""
#import code mods
import sys
import uuid
import gtk, pygtk
from PyQt4 import QtGui, QtCore
from array import *
#import StyleMod
import RRG_NodeInterfaceGUIStyle as ns
import RRG_importNodeData as ind
"""
Base class for a node. Contains all the inilization, drawing, and containing inputs and outputs
"""
class node(QtGui.QGraphicsRectItem):
nid = 0
width = ns.nodeWidth
height = ns.nodeHeight
color = ns.nodeBackgroundColor
alpha = ns.nodeBackgroundAlpha
x = 90
y = 60
inputs=[]
outputs=[]
viewObj = None
isNode = True
scene = None
def __init__(self, n_x, n_y, n_width,n_height, n_scene):
QtGui.QGraphicsRectItem.__init__(self, n_x, n_y, n_width, n_height)
self.width = n_width
self.height = n_height
self.x = n_x
self.y = n_y
self.scene = n_scene
self.nid = uuid.uuid4()
print(self.nid)
self.setFlag(QtGui.QGraphicsItem.ItemIsMovable, True)
self.iniNodeData()
def mousePressEvent(self, e):
print("Square got mouse press event")
print("Event came to us accepted: %s"%(e.isAccepted(),))
a = []
self.scene.selectedNodes = a
self.scene.selectedNodes.append(self)
self.scene.selectedNodeID = self.nid
QtGui.QGraphicsRectItem.mousePressEvent(self, e)
def mouseReleaseEvent(self, e):
#print("Square got mouse release event")
#print("Event came to us accepted: %s"%(e.isAccepted(),))
QtGui.QGraphicsRectItem.mouseReleaseEvent(self, e)
"""
This is where inputs and outputs will be created based on node type
"""
def iniNodeData(self):
print('making node data')
for j in range(5):
this = self
x = input(this,0, 0+(j*10),self.scene)
self.inputs.append(x)
for k in range(5):
this = self
x = output(this, self.width-10, 0+(k*10),self.scene)
self.outputs.append(x)
def mouseMoveEvent(self, event):
self.scene.updateConnections()
QtGui.QGraphicsRectItem.mouseMoveEvent(self, event)
def nid(self):
return self.nid
"""
Nodes will evaluate from the last node to the first node, therefore inputs are evaluted
"""
class input(QtGui.QGraphicsRectItem):
currentConnectedNode = None
currentConnectedOutput = None
parentNode = None
width = 10
height = 10
x = 1
y = 1
color = 1
drawItem = None
isOutput = False
isNode = False
scene = None
points = []
line = None
def __init__(self, pnode, posX, posY, n_scene):
self.parentNode = pnode
self.x = posX
self.y = posY
self.color = 1
self.scene = n_scene
QtGui.QGraphicsRectItem.__init__(self, self.x+self.parentNode.x, self.y+self.parentNode.y, self.width, self.height, self.parentNode)
self.setFlag(QtGui.QGraphicsItem.ItemIsSelectable, True)
'''
This will handel connections. It determins if a connection is allowed aswell as creates them
'''
def mousePressEvent(self, e):
#print("Square got mouse press event")
#print("Event came to us accepted: %s"%(e.isAccepted(),))
emptyArray = []
if len(self.scene.selectedNodes) > 0 or len(self.scene.selectedNodes) > 1:
a = self.scene.selectedNodes[0]
if a.isNode == False:
if a.parentNode != self.parentNode:
if a.isOutput == True and self.isOutput == False:
print('Output and Input! line test runnin....')
currentConnectedOutput = a
currentConnectedNode = a.parentNode
if self.line != None:
self.line = None
self.scene.addConnection(self, a)
elif a.isOutput == True and self.isOutput == True:
print('Output and Output')
elif a.isOutput == False and self.isOutput == False:
print('Input and Input')
self.scene.selectedNodes = emptyArray
else:
self.scene.selectedNodes = emptyArray
else:
self.scene.selectedNodes = emptyArray
self.scene.selectedNodes.append(self)
else:
self.scene.selectedNodes.append(self)
QtGui.QGraphicsRectItem.mousePressEvent(self, e)
'''
Output value from a node
'''
class output(QtGui.QGraphicsRectItem):
parentNode = None
width = 10
height = 10
x = 0
y = 0
isOutput = True
isNode = False
scene = None
def __init__(self, pnode, posX, posY, n_scene):
self.parentNode = pnode
self.x = posX
self.y = posY
self.color = 1
self.scene = n_scene
QtGui.QGraphicsRectItem.__init__(self, self.x+self.parentNode.x, self.y+self.parentNode.y, self.width, self.height, self.parentNode)
self.setFlag(QtGui.QGraphicsItem.ItemIsSelectable, True)
'''
This will handel connections. It determins if a connection is allowed aswell as creates them
'''
def mousePressEvent(self, e):
#print("Square got mouse press event")
#print("Event came to us accepted: %s"%(e.isAccepted(),))
emptyArray = []
if len(self.scene.selectedNodes) > 0 or len(self.scene.selectedNodes) > 1:
a = self.scene.selectedNodes[0]
if a.isNode == False:
if a.parentNode != self.parentNode:
if a.isOutput == False and self.isOutput == True:
print('Input and Output!')
a.currentConnectedOutput = self
a.currentConnectedNode = self.parentNode
elif a.isOutput == True and self.isOutput == False:
print('Output and Input!')
elif a.isOutput == True and self.isOutput == True:
print('Output and Output')
elif a.isOutput == False and self.isOutput == False:
print('Input and Input')
self.scene.selectedNodes = emptyArray
else:
self.scene.selectedNodes = emptyArray
else:
self.scene.selectedNodes = emptyArray
self.scene.selectedNodes.append(self)
else:
self.scene.selectedNodes.append(self)
QtGui.QGraphicsRectItem.mousePressEvent(self, e)
class connection(QtGui.QGraphicsLineItem):
usedNodeIDs = []
inputItem = None
outputItem = None
x1 = 0.0
y1 = 0.0
x2 = 0.0
y2 = 0.0
nid = None
scene = None
def __init__(self, n_inputItem, n_outputItemm, n_scene):
self.inputItem = n_inputItem
self.outputItem = n_outputItemm
self.usedNodeIDs.append(self.inputItem.parentNode.nid)
self.usedNodeIDs.append(self.outputItem.parentNode.nid)
self.updatePos()
QtGui.QGraphicsLineItem.__init__(self, self.x1, self.y1, self.x2, self.y2)
self.setFlag(QtGui.QGraphicsItem.ItemIsSelectable, True)
self.scene = n_scene
self.nid = uuid.uuid4()
def update(self):
self.updatePos()
self.setLine(self.x1, self.y1, self.x2, self.y2)
def updatePos(self):
scenePosInput = QtGui.QGraphicsItem.pos(self.inputItem)
scenePosOutput = QtGui.QGraphicsItem.pos(self.outputItem)
scenePosInputNode = QtGui.QGraphicsItem.pos(self.inputItem.parentNode)
scenePosOutputNode = QtGui.QGraphicsItem.pos(self.outputItem.parentNode)
self.x1 = (scenePosInputNode.x()+self.inputItem.parentNode.x)+(scenePosInput.x()+self.inputItem.x) + ns.inputWidth/2
self.y1 = (scenePosInputNode.y()+self.inputItem.parentNode.y)+(scenePosInput.y()+self.inputItem.y) + ns.inputHeight/2
self.x2 = (scenePosOutputNode.x()+self.outputItem.parentNode.x)+(scenePosOutput.x()+self.outputItem.x) + ns.outputWidth/2
self.y2 = (scenePosOutputNode.y()+self.outputItem.parentNode.y)+(scenePosOutput.y()+self.outputItem.y) + ns.outputHeight/2
def mousePressEvent(self, e):
self.scene.selectedNodeID = self.nid
QtGui.QGraphicsLineItem.mousePressEvent(self, e)
'''
Check Click events on the scene Object
Also Stores the node data
'''
class Scene(QtGui.QGraphicsScene):
nodes = []
connections = []
selectedNodeID = None
def __init__(self, x, y, w, h, p):
super(Scene, self).__init__()
self.width = w
self.height = h
def mousePressEvent(self, e):
#print("Scene got mouse press event")
#print("Event came to us accepted: %s"%(e.isAccepted(),))
QtGui.QGraphicsScene.mousePressEvent(self, e)
def mouseReleaseEvent(self, e):
#print("Scene got mouse release event")
#print("Event came to us accepted: %s"%(e.isAccepted(),))
QtGui.QGraphicsScene.mouseReleaseEvent(self, e)
def dragMoveEvent(self, e):
QtGui.QGraphicsScene.dragMoveEvent(self, e)
def updateConnections(self):
for connect in self.connections:
connect.update()
def addNode(self):
newNode = node(250,250,100,150, self)
self.addItem(newNode)
self.nodes.append(newNode)
def addPatternNode(self):
newNode = node(250,250,100,150, self)
self.addItem(newNode)
self.nodes.append(newNode)
def addConnection(self, n_inputItem, n_outputItem):
newConnection = connection(n_inputItem, n_outputItem, self)
self.addItem(newConnection)
self.connections.append(newConnection)
def keyPressEvent(self, e):
#Delete a node after it have been clicked on
#Use the node ID as the unique ID of the node to delete
if e.key() == QtCore.Qt.Key_Delete or e.key() == QtCore.Qt.Key_Backspace:
#If nothing selected
if self.selectedNodeID != None:
isConnection = False
for j in range(len(self.connections)):
if self.connections[j].nid == self.selectedNodeID:
isConnection = True
self.removeItem(self.connections[j])
self.connections.remove(self.connections[j])
if isConnection != True:
#first remove connections
rmItem = False
connectionsToRemove = []
for connect in self.connections:
rmItem = False
for nid in connect.usedNodeIDs:
if nid == self.selectedNodeID:
if rmItem == False:
connectionsToRemove.append(connect)
rmItem = True
for removeThis in connectionsToRemove:
self.connections.remove(removeThis)
self.removeItem(removeThis)
#now remove the nodes
for j in range(len(self.nodes)):
print(self.nodes[j].nid)
#figure out which node in our master node list must be deleted
if self.nodes[j].nid == self.selectedNodeID:
self.removeItem(self.nodes[j])
self.nodes.remove(self.nodes[j])
self.selectedNodeID = None
class mainWindowWidget(QtGui.QWidget):
grid = None
scene = None
def __init__(self):
self.initScene()
QtGui.QWidget.__init__(self)
def initScene(self):
self.grid = QtGui.QGridLayout()
'''Node Interface'''
self.scene = Scene(0, 0, 1280, 720, self)
self.view = QtGui.QGraphicsView()
self.view.setScene(self.scene)
self.grid.addWidget(self.view)
'''AttributeWindow'''
class MainWindowUi(QtGui.QMainWindow):
def __init__(self):
mainDataGraber = ind.dataGraber()
QtGui.QMainWindow.__init__(self)
self.setWindowTitle('RIS RIB Generator')
mainwindowwidget = mainWindowWidget()
self.setCentralWidget(mainwindowwidget)
exitAction = QtGui.QAction(QtGui.QIcon('exit24.png'), 'Exit', self)
exitAction.setShortcut('Ctrl+Q')
exitAction.setStatusTip('Exit application')
exitAction.triggered.connect(self.close)
newNodeAction = QtGui.QAction(QtGui.QIcon('exit24.png'), 'New Node', self)
newNodeAction.setStatusTip('Add a blank node')
newNodeAction.triggered.connect(mainwindowwidget.scene.addPatternNode)
nodeCreationActions = []
for nodeType in mainDataGraber.abstractNodeObjects:
nodeName = nodeType.nName
nodeType = nodeType.nType
#nodeStatusTip = nodeType.nhelp
newNodeAction = QtGui.QAction(QtGui.QIcon('exit24.png'), nodeName, self)
newNodeAction.setStatusTip('nodeType.nhelp')
if nodeType == 'pattern':
newNodeAction.triggered.connect(mainwindowwidget.scene.addPatternNode)
nodeCreationActions.append(newNodeAction)
newNodeAction = QtGui.QAction(QtGui.QIcon('exit24.png'), 'New Node', self)
newNodeAction.setStatusTip('Add a blank node')
newNodeAction.triggered.connect(mainwindowwidget.scene.addPatternNode)
self.statusBar()
menubar = self.menuBar()
fileMenu = menubar.addMenu('&File')
fileMenu.addAction(newNodeAction)
nodeMenu = menubar.addMenu('&Nodes')
for action in nodeCreationActions:
nodeMenu.addAction(action)
fileMenu.addAction(exitAction)
'''
Start Point
'''
if __name__ == '__main__':
app = QtGui.QApplication(sys.argv)
win = MainWindowUi()
win.show()
sys.exit(app.exec_())
Your issue is that you don't specify a parent for the QGridLayout (in the mainWindowWidget class), so it isn't attached to a widget. This results in the layout (and all widgets contained within it) not being visible. Adding a parent to the layout reveals a second problem in that you try and do things with the QWidget before calling __init__.
The corrected code is thus:
class mainWindowWidget(QtGui.QWidget):
grid = None
scene = None
def __init__(self):
QtGui.QWidget.__init__(self)
self.initScene()
def initScene(self):
self.grid = QtGui.QGridLayout(self)
'''Node Interface'''
self.scene = Scene(0, 0, 1280, 720, self)
self.view = QtGui.QGraphicsView()
self.view.setScene(self.scene)
self.grid.addWidget(self.view)
Note: For future questions requiring debugging help, please make a minimilistic example that is runnable. Don't just dump 90% of your code in a stack overflow post. It's not fun trying to dig through random code trying to cut out the missing imports so that it still reproduces the problem (fortunately it wasn't too difficult in this case). See How to create a Minimal, Complete, and Verifiable example.
Note 2: Why are you importing pygtk into a qt app?