how to use wx.Slider with SELRANGE? - python

In the docs for wx.Slider (wxPython for py2, wxPython for py3, wxWidgets), there is listed a widget control named wx.SL_SELRANGE, defined to allow "the user to select a range on the slider (MSW only)". To me, this speaks of a twin-control, two sliders on the same axis in order to define a low/high range. I can't get it to show two controls.
Basic code to get it started. I'm not even worried yet about methods, events, or whatnot at this point, just to show something.
class MyFrame(wx.Frame):
def __init__(self, *args, **kwds):
# ... sizers and other stuff
self.myslider = wx.Slider(self.notebook_1_pane_2, wx.ID_ANY, 0, -100, 100, style=wx.SL_SELRANGE)
# ...
self.myslider.SetSelection(10, 90)
With all of that, the most I've been able to get it to show is a blue line spanning about where I would expect things to be.
The wxPython docs all talk about it but how is the user supposed to be able to "select a range on the slider", like shown here (taken from shiny)?
What am I missing? Are there any reasonable public examples of a wxPython wx.Slider in the wild with this functionality?
PS:
One page I found speaks of WinXP only, but since that page hasn't been updated in seven years, I don't consider it authoritative on the version restriction.
I've been using wxGlade for gui layout, but I'm certainly willing/able to go into the code after export and muck around.
System: win81_64, python-2.7.10, wxPython-3.0.2.0

I have made a custom implementation for this, partly using a code from this question. Left click on the slider area sets the left border of the range, right click sets the right border. Dragging the slider moves the selection. left_gap and right_gap indicates what is the empty space between edges of the widget and actual start of the drawn slider. As in the source, these must be found out by experimentation.
class RangeSlider(wx.Slider):
def __init__(self, left_gap, right_gap, *args, **kwargs):
wx.Slider.__init__(self, *args, **kwargs)
self.left_gap = left_gap
self.right_gap = right_gap
self.Bind(wx.EVT_LEFT_UP, self.on_left_click)
self.Bind(wx.EVT_RIGHT_UP, self.on_right_click)
self.Bind(wx.EVT_SCROLL_PAGEUP, self.on_pageup)
self.Bind(wx.EVT_SCROLL_PAGEDOWN, self.on_pagedown)
self.Bind(wx.EVT_SCROLL_THUMBTRACK, self.on_slide)
self.slider_value=self.Value
self.is_dragging=False
def linapp(self, x1, x2, y1, y2, x):
proportion=float(x - x1) / (x2 - x1)
length = y2 - y1
return round(proportion*length + y1)
# if left click set the start of selection
def on_left_click(self, e):
if not self.is_dragging: #if this wasn't a dragging operation
position = self.get_position(e)
if position <= self.SelEnd:
self.SetSelection(position, self.SelEnd)
else:
self.SetSelection(self.SelEnd, position)
else:
self.is_dragging = False
e.Skip()
# if right click set the end of selection
def on_right_click(self, e):
position = self.get_position(e)
if position >= self.SelStart:
self.SetSelection(self.SelStart, position)
else:
self.SetSelection(position, self.SelStart)
e.Skip()
# drag the selection along when sliding
def on_slide(self, e):
self.is_dragging=True
delta_distance=self.Value-self.slider_value
self.SetSelection(self.SelStart+delta_distance, self.SelEnd+delta_distance)
self.slider_value=self.Value
# disable pageup and pagedown using following functions
def on_pageup(self, e):
self.SetValue(self.Value+self.PageSize)
def on_pagedown(self, e):
self.SetValue(self.Value-self.PageSize)
# get click position on the slider scale
def get_position(self, e):
click_min = self.left_gap #standard size 9
click_max = self.GetSize()[0] - self.right_gap #standard size 55
click_position = e.GetX()
result_min = self.GetMin()
result_max = self.GetMax()
if click_position > click_min and click_position < click_max:
result = self.linapp(click_min, click_max,
result_min, result_max,
click_position)
elif click_position <= click_min:
result = result_min
else:
result = result_max
return result

I had this same problem before and couldn't find a good solution. What I ended up doing was creating my own custom RangeSlider widget with two actual thumbs.
Code is available in this answer, or in this GitHub gist.

Better late than never: to answer the original question, wxSL_SELRANGE does work but it only results in the expected appearance if it's combined with wxSL_LABELS. With both of these styles (and the selection set to 20..80 for the total range 0..100) the control appears like this:

Related

How do I determine the number of visible items in a listbox with urwid?

I'd like to implement some hinting as to whether there remains items below or above the list of visible items in an urwid.ListBox when I scroll it up or down. The 'scroll down' hint should appear only when there remains items after the last visible item and it should disappear when the last, visible item is the last item in the list. The reverse applies with the 'scroll up' hint.
I then need to know how many visible items there is in the list. Is there a way to retrieve the number of visible items in a list box, which I suppose is equal to the height of the list box, right?
Here's a starting point of what I'd like to check:
# This example is based on https://cmsdk.com/python/is-there-a-focus-changed-event-in-urwid.html
import urwid
def callback():
index = str(listbox.get_focus()[1])
debug.set_text("Index of selected item: " + index)
captions = "A B C D E F".split()
debug = urwid.Text("Debug")
items = [urwid.Button(caption) for caption in captions]
walker = urwid.SimpleListWalker(items)
listbox = urwid.ListBox(walker)
urwid.connect_signal(walker, "modified", callback)
frame = urwid.Frame(body=listbox, header=debug)
urwid.MainLoop(frame).run()
The idea is to know if the listbox is fully visible within the frame when the terminal window is shrunk or not tall enough to display everything, i.e. frame.height >= listbox.height .
So, here is one way of doing this by subclassing urwid.ListBox, we can add an attribute all_children_visible which is set at the times when we know the size of the widget (that is, when rendering or when handling an input event).
The sample code, based on the sample you provided:
import string
import urwid
class MyListBox(urwid.ListBox):
all_children_visible = True
def keypress(self, size, *args, **kwargs):
self.all_children_visible = self._compute_all_children_visible(size)
return super(MyListBox, self).keypress(size, *args, **kwargs)
def mouse_event(self, size, *args, **kwargs):
self.all_children_visible = self._compute_all_children_visible(size)
return super(MyListBox, self).mouse_event(size, *args, **kwargs)
def render(self, size, *args, **kwargs):
self.all_children_visible = self._compute_all_children_visible(size)
return super(MyListBox, self).render(size, *args, **kwargs)
def _compute_all_children_visible(self, size):
n_total_widgets = len(self.body)
middle, top, bottom = self.calculate_visible(size)
n_visible = len(top[1]) + len(bottom[1])
if middle:
n_visible += 1
return n_total_widgets == n_visible
def callback():
debug.set_text(
"Are all children visible? {}\n".format(listbox.all_children_visible)
)
captions = list(string.uppercase + string.lowercase)
# uncomment this line to test case of all children visible:
# captions = list(string.uppercase)
debug = urwid.Text("Debug")
items = [urwid.Button(caption) for caption in captions]
walker = urwid.SimpleListWalker(items)
listbox = MyListBox(walker)
urwid.connect_signal(walker, "modified", callback)
frame = urwid.Frame(body=listbox, header=debug)
urwid.MainLoop(frame).run()
I'm not sure how well this performs (I haven't tested it extensively), so I'm curious how this will perform for your case -- let me know how it goes. :)

QGraphicsScene changing objects when selected

I have a QGraphicsScene containing some simple objects (in this simplified example circles) that I want to change into other objects (here squares) when selected. More specifically I'd like to have parent objects which don't draw themselves, they are drawn by their child objects, and under various circumstances, but in particular when the parent objects are selected, I'd like the set of child objects to change. This is a nice conceptual framework for the overall app I am working on.
So I've implemented this in PySide and I thought it was working fine: the circles change nicely into squares when you click on them.
Until I use RubberBandDrag selection in the view. This causes an instant segfault when the rubber band selection reaches the parent object and the selection changes. Presumably this is being triggered because the rubber band selection in QT is somehow keeping a pointer to the child item which is disappearing before the rubber band selection action is complete.
Simplified code below - test it by first clicking on the object (it changes nicely) then dragging over the object - segfault:
from PySide import QtCore,QtGui
class SceneObject(QtGui.QGraphicsItem):
def __init__(self, scene):
QtGui.QGraphicsItem.__init__(self, scene = scene)
self.setFlag(QtGui.QGraphicsItem.ItemIsSelectable, True)
self.setFlag(QtGui.QGraphicsItem.ItemHasNoContents, True)
self.updateContents()
def updateContents(self):
self.prepareGeometryChange()
for c in self.childItems():
self.scene().removeItem(c)
if self.isSelected():
shape_item = QtGui.QGraphicsRectItem()
else:
shape_item = QtGui.QGraphicsEllipseItem()
shape_item.setFlag(QtGui.QGraphicsItem.ItemIsSelectable, False)
shape_item.setFlag(QtGui.QGraphicsItem.ItemStacksBehindParent,True)
shape_item.setPen(QtGui.QPen("green"))
shape_item.setRect(QtCore.QRectF(0,0,10,10))
shape_item.setParentItem(self)
def itemChange(self, change, value):
if self.scene() != None:
if change == QtGui.QGraphicsItem.ItemSelectedHasChanged:
self.updateContents()
return
return super(SceneObject,self).itemChange(change, value)
def boundingRect(self):
return self.childrenBoundingRect()
class Visualiser(QtGui.QMainWindow):
def __init__(self):
super(Visualiser,self).__init__()
self.viewer = QtGui.QGraphicsView(self)
self.viewer.setDragMode(QtGui.QGraphicsView.RubberBandDrag)
self.setCentralWidget(self.viewer)
self.viewer.setScene(QtGui.QGraphicsScene())
parent_item = SceneObject(self.viewer.scene())
parent_item.setPos(50,50)
app = QtGui.QApplication([])
mainwindow = Visualiser()
mainwindow.show()
app.exec_()
So questions:
Have I just made a mistake that can be straightforwardly fixed?
Or is removing objects from the scene not allowed when handling an ItemSelectedHasChanged event?
Is there a handy workaround? Or what's a good alternative approach? I could replace the QGraphicsRectItem with a custom item which can be drawn either as a square or a circle but that doesn't conveniently cover all my use cases. I can see that I could make that work but it will certainly not be as straightforward.
EDIT - Workaround:
It is possible to prevent this failing by preserving the about-to-be-deleted object for a while. This can be done by something like this:
def updateContents(self):
self.prepareGeometryChange()
self._temp_store = self.childItems()
for c in self.childItems():
self.scene().removeItem(c)
...
However, this is ugly code and increases the memory usage for no real benefit. Instead I have moved to using the QGraphicsScene.selectionChanged signal as suggested in this answer.
I've debugged it. Reproduced on Lunix
1 qFatal(const char *, ...) *plt 0x7f05d4e81c40
2 qt_assert qglobal.cpp 2054 0x7f05d4ea197e
3 QScopedPointer<QGraphicsItemPrivate, QScopedPointerDeleter<QGraphicsItemPrivate>>::operator-> qscopedpointer.h 112 0x7f05d2c767ec
4 QGraphicsItem::flags qgraphicsitem.cpp 1799 0x7f05d2c573b8
5 QGraphicsScene::setSelectionArea qgraphicsscene.cpp 2381 0x7f05d2c94893
6 QGraphicsView::mouseMoveEvent qgraphicsview.cpp 3257 0x7f05d2cca553
7 QGraphicsViewWrapper::mouseMoveEvent qgraphicsview_wrapper.cpp 1023 0x7f05d362be83
8 QWidget::event qwidget.cpp 8374 0x7f05d2570371
qt-everywhere-opensource-src-4.8.6/src/gui/graphicsview/qgraphicsscene.cpp:2381
void QGraphicsScene::setSelectionArea(const QPainterPath &path, Qt::ItemSelectionMode mode,
const QTransform &deviceTransform)
{
...
// Set all items in path to selected.
foreach (QGraphicsItem *item, items(path, mode, Qt::DescendingOrder, deviceTransform)) {
if (item->flags() & QGraphicsItem::ItemIsSelectable) { // item is invalid here
if (!item->isSelected())
changed = true;
unselectItems.remove(item);
item->setSelected(true);
}
}
They are using items() function to find a list of items under the rubber band selection. But if one item while processing deletes something the item pointer just becomes invalid. And next call to item->flags() causes the crash.
As alternative you could use QGraphicsScene::selectionChanged signal. It's emitted only once per selection change.
Looks like it's not expected by Qt to have some major changes in itemChange
Behind of this here is common mistake you have with prepareGeometryChange() call.
It's designed to be called right before changing boundingRect. Bounding rect should be the old one when prepareGeometryChange called and new one right after.
So that's could happen:
In updateContents:
self.prepareGeometryChange(); # calls boundingRect. old value returned
...
shape_item.setParentItem(self); # could call the boundingRect. but still old value returned!
After child added it calls boundingRect again but value unexpected different.
As a solution you can add a variable
def updateContents(self):
for c in self.childItems():
self.scene().removeItem(c)
if self.isSelected():
shape_item = QtGui.QGraphicsRectItem()
else:
shape_item = QtGui.QGraphicsEllipseItem()
shape_item.setFlag(QtGui.QGraphicsItem.ItemIsSelectable, False)
shape_item.setFlag(QtGui.QGraphicsItem.ItemStacksBehindParent,True)
shape_item.setPen(QtGui.QPen("green"))
shape_item.setRect(QtCore.QRectF(0,0,10,10))
shape_item.setParentItem(self)
self.prepareGeometryChange();
self._childRect = self.childrenBoundingRect()
def boundingRect(self):
return self._childRect

Best approach to draw and update thousands of QGraphicsRectItem(s) efficiently?

I'm new to Qt (PySide), and I'm trying to draw a 'grid map' efficiently. However my solution slows down to a halt with 10k+ QGraphicsRectItem.
Currently it works like so:
class GridMapView(QObject, QGraphicsItemGroup):
def __init__(self, mapWidth, mapHeight, cellSize):
QObject.__init__(self)
QGraphicsItemGroup.__init__(self)
self.mapWidth = mapWidth
self.mapHeight = mapHeight
self.cellSize = cellSize
self.graphicCells = []
#Create cells.
for x in range(self.mapWidth / self.cellSize):
self.graphicCells.append([])
for y in range(self.mapHeight / self.cellSize):
self.graphicCells[x].append(QGraphicsRectItem(x * self.cellSize, y * self.cellSize, self.cellSize, self.cellSize))
self.graphicCells[x][-1].setBrush(QBrush(QColor('grey')))
self.addToGroup(self.graphicCells[x][-1])
self.setPos(-mapWidth/2, -mapHeight/2)
#Slot(Point, int)
def onCellUpdated(self, index, state):
cell = self.graphicCells[index.x][index.y]
if state == CellStates.UNKNOWN:
cell.setBrush(QBrush(QColor('grey')))
cell.setVisible(True)
elif state == CellStates.FREE:
cell.setVisible(False)
elif state == CellStates.OCCUPIED:
cell.setBrush(QBrush(QColor('black')))
cell.setVisible(True)
The initial grid is populated during creation. When the appropriate signal is fired, a specific cell will be updated. This updating is fairly infrequent, and my assumption was that Qt only draws what changes.
The entire 'map' is visible in my viewport, and disabling the rendering makes my application run perfectly fine.
I've tried setting QGraphicsView.NoViewportUpdate, yet it still updates the entire view. I hoped it would require me to call '.update()'.
Is this approach flawed from the start? Thanks in advance.

Qprogressbar with two values

I have some unusual question :
For visualization of packing progress i think about qprogressbar with two values in one bar - one showing bytes read, and another showing write-out bytes, which gives also imagine about compress ratio.
It is possible with QT4 ?
Also, I have very little experience with C++ coding, my current work is based on Python, PyQT4,
Yes it's possible, but you will have to implement your own "DualValueProgressbar" here you have an example, is not complete production code but it will point to you in the right direction.
A note before continue:
Will this you will be able to show two values in the bar, but show two colours in the same bar is a very different thing. So I'll recomend you to use two prograssbar for doing what you want, keep it simple.
Before see any code let me explain what I did.
Subclass QProgressBar
Add a variable member called self.__value_1. This will be the second value.
Override the method paintEvent in order to draw self.__value_1 inside the bar.
Recomendations:
Write code for establishing limits on the second value. (Minimun and maximun)
Write code for handle the format property.
Write code for habdle the aligment property.
This is the result:
Here is the code:
from PyQt4.QtGui import *
from PyQt4.QtCore import *
class DualValueProgressBar(QProgressBar):
def __init__(self, parent=None):
super(DualValueProgressBar, self).__init__(parent)
# The other value you want to show
self.__value_1 = 0
def paintEvent(self, event):
# Paint the parent.
super(DualValueProgressBar, self).paintEvent(event)
# In the future versions if your custom object you
# should use this to set the position of the value_1
# in the progressbar, right now I'm not using it.
aligment = self.alignment()
geometry = self.rect() # You use this to set the position of the text.
# Start to paint.
qp = QPainter()
qp.begin(self)
qp.drawText(geometry.center().x() + 20, geometry.center().y() + qp.fontMetrics().height()/2.0, "{0}%".format(str(self.value1)))
qp.end()
#property
def value1(self):
return self.__value_1
#pyqtSlot("int")
def setValue1(self, value):
self.__value_1 = value
if __name__ == '__main__':
import sys
app = QApplication(sys.argv)
window = QWidget()
hlayout = QHBoxLayout(window)
dpb = DualValueProgressBar(window)
dpb.setAlignment(Qt.AlignHCenter)
# This two lines are important.
dpb.setValue(20)
dpb.setValue1(10) # Look you can set another value.
hlayout.addWidget(dpb)
window.setLayout(hlayout)
window.show()
sys.exit(app.exec())
Finally the code sample:

Fix values that are typed into a doublespinbox in PyQt

I want to create a doublespin box that changes values in steps of 0.2. But when the user enters a value that is not correct according to the steps. I normalizes that to the nearest correct value.
I tried something like the code shown below but I don't know how to stop values like 0.5 to be entered. Please help me on this.
from PyQt4.QtCore import *
from PyQt4.QtGui import *
class SigSlot(QWidget):
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.setWindowTitle('spinbox value')
self.resize(250,150)
self.lcd1 = QLCDNumber(self)
self.spinbox1 = QDoubleSpinBox(self)
self.spinbox1.setSingleStep(0.2)
self.spinbox1.setCorrectionMode(1)
# create a Grid Layout
grid = QGridLayout()
grid.addWidget(self.lcd1, 0, 0)
grid.addWidget(self.spinbox1, 1, 0)
self.setLayout(grid)
# allows access to the spinbox value as it changes
self.connect(self.spinbox1, SIGNAL('valueChanged(double)'), self.change_value1)
def change_value1(self, event):
val = self.spinbox1.value()
self.lcd1.display(val)
app = QApplication([])
qb = SigSlot()
qb.show()
app.exec_()
You have two choices:
You can subclass the QSpinBox, override validate method and use an appropriate Q*Validator (e.g. QRegExpValidator) inside.
You can check the value in slot connected to valueChanged before using and correct it if necessary.
Since you are already using the valueChanged signal, second option should be fairly easy to implement. Just change your change_value method like this:
def change_value1(self, val): # new value is passed as an argument
# so no need for this
# val = self.spinbox1.value()
new_val = round(val*5)/5 # one way to fix
if val != new_val: # if value is changed, put it in the spinbox
self.spinbox1.setValue(new_val)
self.lcd1.display(new_val)
By the way, since you are using only one decimal precision, it might be logical to also use:
self.spinbox1.setDecimals(1)
in your __init__. And try to use the new style signals and slots. i.e.:
self.connect(self.spinbox1, SIGNAL('valueChanged(double)'), self.change_value1)
could be written as:
self.spinbox1.valueChanged[float].connect(self.change_value1)
Edit
Subclassing:
class MySpinBox(QDoubleSpinBox):
def __init__(self, parent=None):
super(MySpinBox, self).__init__(parent)
# any RegExp that matches the allowed input
self.validator = QRegExpValidator(QRegExp("\\d+[\\.]{0,1}[02468]{0,1}"), self)
def validate(self, text, pos):
# this decides if the entered value should be accepted
return self.validator.validate(text, pos)
then instead of using QDoubleSpinBox you would use MySpinBox and leave the input checking to this class.
In your change value method you can do something like this
val = round(self.spinbox1.value(), 1)
if val/2*10 - int(val/2*10):
val = round(val, 1) + .1
It's probably not the best way but it works.

Categories