StackLayout changes size automagically in kivy framework - python

I am using Win7, Eclipse, python 2.7 + kivy framework for developing a openGL app.
I am trying to check some collision points(x, y) of a Widget on a mouse click. I create a layout with a specific size, but when entering the on_touch_down(mouse click) callback the layout size is strangely changed(here is the problem). Let me show you my code:
class PlayScreen(Screen):
layout = None
def __init__(self):
self.layout = StackLayout(size=(Types.SCREEN_SIZE_WIDTH, 100))
#then I create widgets inside the layout:
self.lblScore = Label(text='SCORE:', size_hint=(.1, .1))
self.lblScoreValue = Label(text='0', size_hint=(.1, .1))
.....
self.layout.add_widget(self.lblScore)
self.layout.add_widget(self.lblScoreValue)
#here the debugger shows me self.layout size to be(800, 100)
#and then I want to test if I click on the self.layout:
def on_touch_down(self, touch):
bCanAddTower = True
if self.layout.collide_point(touch.x, touch.y) == True:
print "colision with menu"
#here self.layout has size=(800, 600)(here is the problem) the entire window size, and of course I get into collision all the time.
Does anybody have any idea why the size of the self.layout changes in on_touch_down method?

The initial size is because you instantiate the widget with that size, so all the code you run immediately aftewards sees this value. However, its parent widget is a Screen which is a Layout class (specifically a RelativeLayout) that automatically resizes its children to fill itself unless you set some other options. This automatic resizing only takes place after its __init__ (but before the next frame), which is why on_touch_down, or any other method, will see the new size.
In this case, you can add size_hint=(None, None) to the self.layout instantiation. This simply tells the parent Screen not to manually control its size, so it will remain as you have set it.
In the longer term, you may want to work with some proportional size setting rather than a fixed value, as a totally fixed size won't appear the same way on different screen sizes, pixel densities etc.

Related

How to add an invisible line in PyQt5

This attached image is the screenshot of an application developed using PyQt5.
The image clearly has an invisible line running in the middle of the boxes enclosing the contents.
What code should I add in my program to draw an invisible line overlaying all other objects created earlier. I couldn't find any documentation regarding this but as the image suggests, it has somehow been implemented.
A code snippet is not needed to be provided by me since this is a question about adding/developing a feature rather than debugging or changing any existing code.
Premise: what you provided as an example doesn't seem a very good thing to do. It also seems more a glich than a "feature", and adding "invisible" lines like that might result in an annoying GUI for the user. The only scenario in which I'd use it would be a purely graphical/fancy one, for which you actually want to create a "glitch" for some reason. Also, note that the following solutions are not easy, and their usage requires you an advanced skill level and experience with Qt, because if you don't really understand what's happening, you'll most certainly encounter bugs or unexpected results that will be very difficult to fix.
Now. You can't actually "paint an invisible line", but there are certain work arounds that can get you a similar result, depending on the situation.
The main problem is that painting (at least on Qt) happens from the "bottom" of each widget, and each child widget is painted over the previous painting process, in reverse stacking order: if you have widgets that overlap, the topmost one will paint over the other. This is more clear if you have a container widget (such as a QFrame or a QGroupBox) with a background color and its children use another one: the background of the children will be painted over the parent's.
The (theoretically) most simple solution is to have a child widget that is not added to the main widget layout manager.
Two important notes:
The following will only work if applied to the topmost widget on which the "invisible line" must be applied.
If the widget on which you apply this is not the top level window, the line will probably not be really invisible.
class TestWithChildLine(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QGridLayout(self)
for row in range(3):
for col in range(6):
layout.addWidget(QtWidgets.QDial(), row, col)
# create a widget child of this one, but *do not add* it to the layout
self.invisibleWidget = QtWidgets.QWidget(self)
# ensure that the widget background is painted
self.invisibleWidget.setAutoFillBackground(True)
# and that it doesn't receive mouse events
self.invisibleWidget.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
def resizeEvent(self, event):
super().resizeEvent(event)
# create a rectangle that will be used for the "invisible" line, wide
# as the main widget but with 10 pixel height, then center it
rect = QtCore.QRect(0, 0, self.width(), 10)
rect.moveCenter(self.rect().center())
# set the geometry of the "invisible" widget to that rectangle
self.invisibleWidget.setGeometry(rect)
Unfortunately, this approach has a big issue: if the background color has an alpha component or uses a pixmap (like many styles do, and you have NO control nor access to it), the result will not be an invisible line.
Here is a screenshot taken using the "Oxygen" style (I set a 20 pixel spacing for the layout); as you can see, the Oxygen style draws a custom gradient for window backgrounds, which will result in a "not invisible line":
The only easy workaround for that is to set the background using stylesheets (changing the palette is not enough, as the style will still use its own way of painting using a gradient derived from the QPalette.Window role):
self.invisibleWidget = QtWidgets.QWidget(self)
self.invisibleWidget.setObjectName('InvisibleLine')
self.invisibleWidget.setAutoFillBackground(True)
self.invisibleWidget.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
self.setStyleSheet('''
TestWithChildFull, #InvisibleLine {
background: lightGray;
}
''')
The selectors are required to avoid stylesheet propagation to child widgets; I used the '#' selector to identify the object name of the "invisible" widget.
As you can see, now we've lost the gradient, but the result works as expected:
Now. There's another, more complicated solution, but that should work with any situation, assuming that you're still using it on a top level window.
This approach still uses the child widget technique, but uses QWidget.render() to paint the current background of the top level window on a QPixmap, and then set that pixmap to the child widget (which now is a QLabel).
The trick is to use the DrawWindowBackground render flag, which allows us to paint the widget without any children. Note that in this case I used a black background, which shows a "lighter" gradient on the borders that better demonstrate the effect:
class TestWithChildLabel(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QGridLayout(self)
layout.setSpacing(40)
for row in range(3):
for col in range(6):
layout.addWidget(QtWidgets.QDial(), row, col)
self.invisibleWidget = QtWidgets.QLabel(self)
self.invisibleWidget.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents)
palette = self.palette()
palette.setColor(palette.Window, QtGui.QColor('black'))
self.setPalette(palette)
def resizeEvent(self, event):
super().resizeEvent(event)
pm = QtGui.QPixmap(self.size())
pm.fill(QtCore.Qt.transparent)
qp = QtGui.QPainter(pm)
maskRect = QtCore.QRect(0, 0, self.width(), 50)
maskRect.moveTop(50)
region = QtGui.QRegion(maskRect)
self.render(qp, maskRect.topLeft(), flags=self.DrawWindowBackground,
sourceRegion=region)
qp.end()
self.invisibleWidget.setPixmap(pm)
self.invisibleWidget.setGeometry(self.rect())
And here is the result:
Finally, an further alternative would be to manually apply a mask to each child widget, according to their position. But that could become really difficult (and possibly hard to manage/debug) if you have complex layouts or a high child count, since you'd need to set (or unset) the mask for all direct children each time a resize event occurs. I won't demonstrate this scenario, as I believe it's too complex and unnecessary.

Create new components on an inherited window in pyqt5

I'm working on a small GUI made in PYQT5. I have a main window with a couple of buttons which open new windows. One of these windows has an embedded matplotlib plot and 2 buttons.
So, from this existing window called "PlotWindow" I want to create a new window called "DynamicPlotWindow" but add more elements (Comboboxes, buttons, methods, etc.). In other words, I want to reuse existing windows and put more components on them. I´m able to create new DynamicPlotWindow windows, but the new components added to it aren´t visible.
Based on this question: PyQt5 Making a subclass widgets the definition of both clases is as follows:
class PlotWindow(QMainWindow): #Matplotlib embeded + 2 buttons
def __init__(self, parent):
super(QMainWindow, self).__init__(parent)
self.width = 1000
self.height = 540
self.setGeometry(10, 10, self.width, self.height)
...
self.show()
...
class DynamicPlotWindow(PlotWindow):
def __init__(self, parent):
super(PlotWindow, self).__init__(parent)
self.btn = QPushButton("Test") # -> Not visible
self.btn.resize(120,30)
self.btn.move(600,800)
...
self.show()
My question is what am I doing wrong here? Is it possible to do it?
Best,
Your code has the following errors:
The botton is not a child of the window so it will not be shown, the solution is to pass it to self as parent
The window has a size of 1000x540 but you want to place the button in the position (600,800) that is clearly outside the height: 800> 540.
The solution is:
self.btn = QPushButton("Test", self)
self.btn.resize(120,30)
self.btn.move(600, 200) # change y coordinate

Qt with Windows 10 transparent widget not working

I am working on put some drawings over some child widgets in a window. So I came up with a transparent overlay widget, making its background transparent, putting it on top of the other windows, painting directly on it. It all worked fine on Mac. However, this overlay widget just won't show up in Windows 10. Here are some code for creating this overlay transparent widget:
class OverlayWidget(QWidget):
def __init__(self, x, y, width, height):
super(OverlayWidget, self).__init__()
self.setWindowFlags( Qt.FramelessWindowHint | Qt.Widget | Qt.WindowStaysOnTopHint | Qt.TransparentMode)
self.setAttribute(Qt.WA_TranslucentBackground)
self.org_x = x
self.org_y = y
self.window_width = width
self.window_height = height
self.setGeometry(self.org_x, self.org_y, self.window_width, self.window_height)
self.setMouseTracking(True)
self.setCursor(Qt.BlankCursor)
Once I set the WA_TranslucentBackground attribute, nothing shows up, even the mouse cursor won't change, which means this widget probably wasn't even loaded. Any suggestions?
EDIT:
It seems that this is related to how this widget is added to its parent. If I create a layout and add it to the layout and then set its parent layout as the one I just created, it did show up. However, doing this contradicts with my purpose of overlaying it on top of another existing layout.
But if I just created this widget by passing the parent widget into it constructor, it doesn't work in Windows, but works in Mac.
This is quite annoying.

Getting a QDoubleDoubleSpinBox to display the (converted) value of a QSlider and still use setTracking(False)

I'm creating a ui for an animation tool I've written. The ui consists of a QDoubleSpinBox and a QSlider. I have connected the QSlider to the QDoubleSpinBox with functions to convert their values. I would like to keep the spinbox updating with the valueChange of the slider. I would also like the spinbox's editingFinished() to update the position of the slider handle. I have this connection working.
There are two modes to the tool; "interactive" and "non-interactive". Interactive calls my animation tool's main function as the slider is moved, with the spinbox's/slider's value as the parameter. Non-interactive mode is meant to call my tool's function only when the slider is released. It looked like setTracking() on the slider was the answer, but of course it breaks the spinbox's behaviour of always showing the value of the slider.
Is there a way to keep the spinbox connected to the slider's value and still use setTracking(False) on the slider too? Is there another way to achieve the behaviour I'm looking for? I'm using Python and Pyside.
# jumping to relevant code
self.spinbox = QtGui.QDoubleSpinBox()
self.spinbox.setRange(0.0, 1.0)
self.spinbox.setDecimals(3)
self.sld = QtGui.QSlider()
self.sld.setRange(0, 1000)
self.sld.valueChanged[int].connect(self.update_spinbox)
self.spinbox.editingFinished.connect(self.update_slider_position)
def update_spinbox(self, value):
self.spinbox.setValue(float(value)/1000)
def update_slider_position(self):
self.sld.setSliderPosition(self.spinbox.value()*1000)
A menu in the ui toggles the setTracking() of the slider.
It may not be as elegant and simple as you want, but it should be the behavior you are after.
from PySide import QtGui
class Window(QtGui.QWidget):
def __init__(self, parent = None):
super(Window, self).__init__(parent)
self.updateFromSpinbox = False # Used so it slider doesn't run function twice
self.spinbox = QtGui.QDoubleSpinBox()
self.spinbox.setRange(0.0, 1.0)
self.spinbox.setDecimals(3)
self.spinbox.setSingleStep(0.1)
self.sld = QtGui.QSlider()
self.sld.setRange(0, 1000)
self.checkButton = QtGui.QPushButton('Set interactive mode')
self.checkButton.setCheckable(True)
self.sld.valueChanged[int].connect(self.update_spinbox)
self.sld.sliderReleased.connect(self.slider_released)
self.spinbox.editingFinished.connect(self.update_slider_position)
mainLayout = QtGui.QVBoxLayout()
mainLayout.addWidget(self.spinbox)
mainLayout.addWidget(self.sld)
mainLayout.addWidget(self.checkButton)
self.setLayout(mainLayout)
self.resize(200, 200)
self.show()
# Runs when interactive mode is disabled
def slider_released(self):
if not self.checkButton.isChecked():
self.myFunction()
def update_spinbox(self, value):
if not self.updateFromSpinbox:
self.spinbox.setValue(float(value)/1000)
if self.checkButton.isChecked():
self.myFunction()
def update_slider_position(self):
self.updateFromSpinbox = True
self.sld.setSliderPosition(self.spinbox.value()*1000)
self.updateFromSpinbox = False
self.myFunction()
# Main function to run when value updates
def myFunction(self):
print self.sld.value()
win = Window()
When you check the button to go in interactive mode, myFunction() will be called when the slider is moving and when the spinbox is done editing. If it's checked off to go out of interactive mode, myFunction() will only be called when the slider is released and the spinbox is done editing.

What is the most efficient way to display multiple pixmaps into a scroll area?

I am trying to make an application that displays a PDF file, using PyQt4 and python-poppler-qt4.
So far I have managed to display the entire document by loading pixmaps generated with Poppler, set on a QLabel and appended to a QFrame. The QFrame is displayed in a QScrollArea.
It looks pretty good, until implementing zooming, which is done by regenerating the pixmaps all over again, with an incremented resolution. This process requires the entire document to be rendered into pixmaps, which obviously takes time and results into an unwanted lag.
Logic wants that I should display images of the pages I am seeing only (it sounds like quantum physics). I have two options in mind:
create blank pages with QLabels and load the image onto them when they become visible in the scroll area;
create only one page and add or remove precedent or subsequent pages right before it should be displayed.
I am not sure I am on the right track or whether there is an alternative.
The first option seems more feasible, because the visibility of a blank page determines when the pixmap has to be uploaded (although I have no idea how to delete that pixmap when the page is hidden). Yet I am not sure that zooming will be faster this way, since a document of, say, 600 pages, will have to be regenerated, albeit with blank pages.
The second option should definitely improve zooming since 1 to 4 pages at a time would have to be regenerated when zooming. In that second case however, I am not sure how to trigger the construction of pages.
What would you suggest?
wouldn't it be simple to forget the QLabels and directly draw the image:
from PyQt4.QtGui import *
import sys
app = QApplication(sys.argv)
class Test(QWidget):
def __init__(self):
super(Test, self).__init__()
self.painter = QPainter()
# placeholder for the real stuff to draw
self.image = QImage("/tmp/test.jpg")
def paintEvent(self, evt):
rect = evt.rect()
evt.accept()
print rect
self.painter.begin(self)
zoomedImage = self.image # ... calculate this for your images
sourceRect = rect # ... caluclate this ...
# draw it directly
self.painter.drawImage(rect, self.image, sourceRect)
self.painter.end()
t = Test()
t.setGeometry(0,0,600,800)
s = QScrollArea()
s.setWidget(t)
s.setGeometry(0,0,300,400)
s.show()
app.exec_()
I've worked out an answer, using option 1 in the question:
def moveEvent(self, event):
self.checkVisibility()
event.ignore()
def resizeEvent(self, event):
self.checkVisibility()
event.ignore()
def checkVisibility(self):
print "Checking visibility"
for page in self.getPages():
if not page.visibleRegion().isEmpty():
if page.was_visible:
pass
else:
print page.page_number, "became visible"
page.was_visible = True
self.applyImageToPage(page)
else:
if page.was_visible:
print page.page_number, "became invisible"
page.was_visible = False
else:
pass
def applyImageToPage(self, page):
print "applying image to page", page.page_number
source = self.getSourcePage(self.getPageNumber(page))
scale = self.display.scale
# this is where the error occurs
image = source.renderToImage(72 * scale, 72 * scale)
pixmap = QtGui.QPixmap.fromImage(image)
page.setPixmap(pixmap)

Categories