Related
Question:
I am struggling for a more than a week now to do something probably pretty simple:
I want to make a time series plot in which i can control the x axis
range/zoom with a datetime picker widget.
I also want the datetime picker to be updated when the x range is
changed with the plot zoom controls
So far I can do either but not both. It did work for other widgets such as the intslider etc.
Requirements:
If the solution has 1 DatetimeRangePicker to define the x range or 2 DatetimePicker widgets (one for start one for end) would both work great for me.
my datasets are huge so it would be great if it works with datashader
Any help is much appreciated :)
What I tried:
MRE & CODE BELOW
Create a DatetimeRangePicker widget, plot the data using pvplot and set the xlim=datatimerangepicker.
Result: the zoom changes with the selected dates on the widget, but zooming / panning the plot does not change the values of the widget.
Use hv.streams.RangeX stream to capture changes in x range when panning / zooming. Use a pn.depends function to generate plot when changing DatetimeRangePicker widget.
Result: the figure loads and zooming/panning changes the widget (but is very slow), but setting the widget causes AttributeError.
Create a DatetimePicker widget for start and end. Link them with widget.jslink() bidirectionally to x_range.start and x_range.end of the figure.
Result: figure loads but nothing changes when changing values on the widget or panning/zooming.
MRE & Failed Attempts
Create Dataset
import pandas as pd
import numpy as np
import panel as pn
import holoviews as hv
import hvplot.pandas
hv.extension('bokeh')
df = pd.DataFrame({'data': np.random.randint(0, 100, 100)}, index=pd.date_range(start="2022", freq='D', periods=100))
Failed Method 1:
plot changes with widget, but widget does not change with plot
range_select = pn.widgets.DatetimeRangePicker(value=(df.index[0], df.index[-1]))
pn.Column(df.data.hvplot.line(datashade=True, xlim=range_select), range_select)
Failed Method 2:
Slow and causes AttributeError: 'NoneType' object has no attribute 'id' when changing widget
range_select = pn.widgets.DatetimeRangePicker(value=(df.index[0], df.index[-1]))
#pn.depends(range_x=range_select.param.value)
def make_fig(range_x):
fig = df.data.hvplot.line(datashade=True, xlim=range_x)
pointer = hv.streams.RangeX(source=fig)
tabl = hv.DynamicMap(show_x, streams=[pointer]) # plot useless table to make it work
return fig + tabl
def show_x(x_range):
if x_range is not None:
range_select.value = tuple([pd.Timestamp(i).to_pydatetime() for i in x_range])
return hv.Table({"start": [x_range[0]], "stop": [x_range[1]]}, ["start", "stop"]) if x_range else hv.Table({})
pn.Column(range_select, make_fig)
Failed Method 3:
does not work with DatetimePicker widget, but does work other widgets (e.g. intslider)
pn.widgets.DatetimePicker._source_transforms = ({}) # see https://discourse.holoviz.org/t/using-jslink-with-pn-widgets-datepicker/1116
# datetime range widgets
range_strt = pn.widgets.DatetimePicker()
range_end = pn.widgets.DatetimePicker()
# int sliders as example that some widgets work
int_start_widget = pn.widgets.IntSlider(start=0, step=int(1e6), end=int(1.7e12))
int_end_widget = pn.widgets.IntSlider(start=0, step=int(1e6), end=int(1.7e12))
points = df.data.hvplot.line(datashade=True) # generate plot
# link widgets to plot:
int_start_widget.jslink(points, value="x_range.start", bidirectional=True)
int_end_widget.jslink(points, value="x_range.end", bidirectional=True)
range_strt.jslink(points, value="x_range.start", bidirectional=True)
range_end.jslink(points, value="x_range.end", bidirectional=True)
pn.Row(points,pn.Column( range_strt, range_end, int_start_widget, int_end_widget,))
Here is what I came up with:
range_select = pn.widgets.DatetimeRangePicker(value=(df.index[0].to_pydatetime(), df.index[-1].to_pydatetime()))
curve = df.data.hvplot.line(datashade=True).apply.opts(xlim=range_select, framewise=True)
rxy = hv.streams.RangeX(source=curve)
def update_widget(event):
new_dates = tuple([pd.Timestamp(i).to_pydatetime() for i in event.new])
if new_dates != range_select.value:
range_select.value = new_dates
rxy.param.watch(update_widget, 'x_range')
pn.Column(range_select, curve)
Basically we use .apply.opts to apply current widget value as the xlim dynamically (and set framewise=True so the plot ranges update dynamically). Then we instantiate a RangeX stream which we use to update the widget value. Annoyingly we have to do some datetime conversions because np.datetime64 and Timestamps aren't supported in some cases.
I am trying to have a user interface using PyQt5 and pyqtgraph. I made two checkboxes and whenever I select them I want to plot one of the two data sets available in the code and whenever I deselect a button I want it to clear the corresponding curve. There are two checkboxes with texts A1 and A2 and each of them plot one set of data.
I have two issues:
1- If I select A1 it plots the data associated with A1 and as long as I do not select A2, by deselecting A1 I can clear the data associated with A1.
However, If I check A1 box and then I check A2 box, then deselecting A1 does not clear the associated plot. In this situation, if I choose to plot random data, instead of a deterministic curve such as sin, I see that by selecting either button new data is added but it cannot be removed.
2- The real application have 96 buttons each of which should be associated to one data set. I think the way I wrote the code is inefficient because I need to copy the same code for one button and data set 96 times. Is there a way to generalize the toy code I presented below to arbitrary number of checkboxes? Or perhaps, using/copying the almost the same code for every button is the usual and correct way to do this?
The code is:
from PyQt5 import QtWidgets, uic, QtGui
import matplotlib.pyplot as plt
from matplotlib.widgets import SpanSelector
import numpy as np
import sys
import string
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtCore
app = QtWidgets.QApplication(sys.argv)
x = np.linspace(0, 3.14, 100)
y1 = np.sin(x)#Data number 1 associated to checkbox A1
y2 = np.cos(x)#Data number 2 associated to checkbox A2
#This function is called whenever the state of checkboxes changes
def todo():
if cbx1.isChecked():
global curve1
curve1 = plot.plot(x, y1, pen = 'r')
else:
try:
plot.removeItem(curve1)
except NameError:
pass
if cbx2.isChecked():
global curve2
curve2 = plot.plot(x, y2, pen = 'y')
else:
try:
plot.removeItem(curve2)
except NameError:
pass
#A widget to hold all of my future widgets
widget_holder = QtGui.QWidget()
#Checkboxes named A1 and A2
cbx1 = QtWidgets.QCheckBox()
cbx1.setText('A1')
cbx1.stateChanged.connect(todo)
cbx2 = QtWidgets.QCheckBox()
cbx2.setText('A2')
cbx2.stateChanged.connect(todo)
#Making a pyqtgraph plot widget
plot = pg.PlotWidget()
#Setting the layout
layout = QtGui.QGridLayout()
widget_holder.setLayout(layout)
#Adding the widgets to the layout
layout.addWidget(cbx1, 0,0)
layout.addWidget(cbx2, 0, 1)
layout.addWidget(plot, 1,0, 3,1)
widget_holder.adjustSize()
widget_holder.show()
sys.exit(app.exec_())
Below is an example I made that works fine.
It can be reused to do more plots without increasing the code, just changing the value of self.num and adding the corresponding data using the function add_data(x,y,ind), where x and y are the values of the data and ind is the index of the box (from 0 to n-1).
import sys
import numpy as np
import pyqtgraph as pg
from pyqtgraph.Qt import QtCore, QtGui
class MyApp(QtGui.QWidget):
def __init__(self):
QtGui.QWidget.__init__(self)
self.central_layout = QtGui.QVBoxLayout()
self.plot_boxes_layout = QtGui.QHBoxLayout()
self.boxes_layout = QtGui.QVBoxLayout()
self.setLayout(self.central_layout)
# Lets create some widgets inside
self.label = QtGui.QLabel('Plots and Checkbox bellow:')
# Here is the plot widget from pyqtgraph
self.plot_widget = pg.PlotWidget()
# Now the Check Boxes (lets make 3 of them)
self.num = 6
self.check_boxes = [QtGui.QCheckBox(f"Box {i+1}") for i in range(self.num)]
# Here will be the data of the plot
self.plot_data = [None for _ in range(self.num)]
# Now we build the entire GUI
self.central_layout.addWidget(self.label)
self.central_layout.addLayout(self.plot_boxes_layout)
self.plot_boxes_layout.addWidget(self.plot_widget)
self.plot_boxes_layout.addLayout(self.boxes_layout)
for i in range(self.num):
self.boxes_layout.addWidget(self.check_boxes[i])
# This will conect each box to the same action
self.check_boxes[i].stateChanged.connect(self.box_changed)
# For optimization let's create a list with the states of the boxes
self.state = [False for _ in range(self.num)]
# Make a list to save the data of each box
self.box_data = [[[0], [0]] for _ in range(self.num)]
x = np.linspace(0, 3.14, 100)
self.add_data(x, np.sin(x), 0)
self.add_data(x, np.cos(x), 1)
self.add_data(x, np.sin(x)+np.cos(x), 2)
self.add_data(x, np.sin(x)**2, 3)
self.add_data(x, np.cos(x)**2, 4)
self.add_data(x, x*0.2, 5)
def add_data(self, x, y, ind):
self.box_data[ind] = [x, y]
if self.plot_data[ind] is not None:
self.plot_data[ind].setData(x, y)
def box_changed(self):
for i in range(self.num):
if self.check_boxes[i].isChecked() != self.state[i]:
self.state[i] = self.check_boxes[i].isChecked()
if self.state[i]:
if self.plot_data[i] is not None:
self.plot_widget.addItem(self.plot_data[i])
else:
self.plot_data[i] = self.plot_widget.plot(*self.box_data[i])
else:
self.plot_widget.removeItem(self.plot_data[i])
break
if __name__ == "__main__":
app = QtGui.QApplication(sys.argv)
window = MyApp()
window.show()
sys.exit(app.exec_())
Note that inside de PlotWidget I add the plot using the plot() method, it returns a PlotDataItem that is saved in the list created before called self.plot_data.
With this, you can easily remove it from the Plot Widget and add it again. Also if you are aiming for a more complex program, for example, one that you can change the data of each box on the run, the plot will update without major issues if you use the setData() method on the PlotDataItem
As I said at the beginning, this should work fine with a lot of checkboxes, because the function that is called when a checkbox is Checked/Unchecked, first compare the actual state of each box with the previous one (stored in self.state) and only do the changes on the plot corresponding to that specific box. With this, you avoid doing one function for each checkbox and the replot of all de boxes every time you check/uncheck a box (like user8408080 did). I don't say it is bad, but if you increase the number of checkboxes and/or the complexity of the data, the workload of replotting all of the data will increase drastically.
The only problem will be when the window is too small to support a crazy amount of checkboxes (96 for example), then you will have to organize the checkboxes in another widget instead of a layout.
Now some screenshots of the code from above:
And then changing the value of self.num to 6 and adding some random data to them:
self.add_data(x, np.sin(x)**2, 3)
self.add_data(x, np.cos(x)**2, 4)
self.add_data(x, x*0.2, 5)
In the following I take a more brute force approach, while assuming, that plotting all the curves takes an negligible amount of time:
import numpy as np
import sys
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtWidgets
app = QtWidgets.QApplication(sys.argv)
x = np.linspace(0, 3.14, 100)
y1 = np.sin(x)#Data number 1 associated to checkbox A1
y2 = np.cos(x)#Data number 2 associated to checkbox A2
curves = [y1, y2]
pens = ["r", "y"]
#This function is called whenever the state of checkboxes changes
def plot_curves(state):
plot.clear()
for checkbox, curve, pen in zip(checkboxes, curves, pens):
if checkbox.isChecked():
plot.plot(x, curve, pen=pen)
#A widget to hold all of my future widgets
widget_holder = QtGui.QWidget()
#Making a pyqtgraph plot widget
plot = pg.PlotWidget()
#Setting the layout
layout = QtGui.QGridLayout()
widget_holder.setLayout(layout)
checkboxes = [QtWidgets.QCheckBox() for i in range(2)]
for i, checkbox in enumerate(checkboxes):
checkbox.setText(f"A{i+1}")
checkbox.stateChanged.connect(plot_curves)
layout.addWidget(checkbox, 0, i)
#Adding the widgets to the layout
layout.addWidget(plot, 1, 0, len(checkboxes), 0)
widget_holder.adjustSize()
widget_holder.show()
sys.exit(app.exec_())
Now you have a list of checkboxes and the checkbox with index 0 corresponds to the data in the curves-list with index 0. I plot all the curves everytime, which yields a little bit more readable code. If this does affect performance, though, this needs to be a little more complicated.
I also tried to add another curve and it seems to work out perfectly fine:
I found the problem in your code. Let's see what your code does:
When you add the first plot to the widget (either A1 or A2) you get the PlotDataItem and store it in curve1 or curve2. Suppose you check first A1, then your todo function first inspects that the checkbox 1 is Checked, so plot the data and store it in curve1, then the same function inspects the checkbox 2. Checkbox 2 is not checked so the function does the else statement, which removes the curve2 from the plot widget, this variable doesn't exist so it might raise an error, however, you use the try statement and the error never raises.
Now, you check the A2 box, your function first inspects checkbox 1, it is checked, so the function will add again the same plot, but as another PlotDataItem, and store it in curve1. Until now, you have two PlotDataItem of the same data (that means two plots) but only the last one is stored in curve1. The next thing the function does is inspect checkbox 2, it is checked so it will plot the second data and save its PlotDataItem in curve2
So, when you now uncheck checkbox 1, your function first inspects checkbox 1 (sorry if it is repetitive), it is unchecked, so the function will remove the PlotDataItem stored in curve1 and it does it, but remember you have two plots of the same data, so for us (the viewers) the plot doesn't disappear. That is the problem, but it doesn't end there, the function now inspects checkbox 2, it is checked, so the function will add another PlotDataItem of the second data and stores it in curve2. We again will have the same problem that happened to the first data.
With this analysis, I also learned something, the PlotDataItem doesn´t disappear if you "overwrite" the variable in which it is stored, neither it does when it is removed from the PlotWidget. Considering that, I did some changes to the code of my previous answer because the old code will create another item each time we check a box that was checked before and was unchecked. Now, if the item is created, my function will add it again, instead of creating another one.
I have some suggestions:
Try using objects, generate your own widget class. You can avoid calling global variables, passing them as attributes of the class. (Like my previous answer)
If you want to maintain your code as it is (without the use of classes), for it to work, you can add another two variables with the "state" of your checkboxes, so when you call your function first it checks if the state didn´t change and ignore that checkbox. Also, check if the PlotDataItem was generated before and only add it again to avoid the generation of more items.
Your objective is to do this with a bunch of boxes or buttons, try using only one variable for all of them: for example, a list, containing all of the boxes/buttons (the objects). Then you can manage any of them by the index. Also, you can do loops over that variable for connecting the objects inside to the same function.
my_buttons = [ QtGui.QPushButton() for _ in range(number_of_buttons) ]
my_boxes= [ QtGui.QCheckBox() for _ in range(number_of_boxes) ]
my_boxes[0].setText('Box 1 Here')
my_boxes[2].setChecked(True)
for i in range(number_of_boxes):
my_boxes[i].stateChanged.connect(some_function)
Doing lists of objects also helps you to give names automatically easily:
my_boxes= [ QtGui.QCheckBox(f"Box number {i+1}") for i in range(number_of_boxes) ]
my_boxes= [ QtGui.QCheckBox(str(i+1)) for i in range(number_of_boxes) ]
my_boxes= [ QtGui.QCheckBox('Box {:d}'.format(i+1)) for i in range(number_of_boxes) ]
Finally, here is your code with some small changes to make it work:
from PyQt5 import QtWidgets, uic, QtGui
import matplotlib.pyplot as plt
from matplotlib.widgets import SpanSelector
import numpy as np
import sys
import string
import pyqtgraph as pg
from pyqtgraph.Qt import QtGui, QtCore
app = QtWidgets.QApplication(sys.argv)
x = np.linspace(0, 3.14, 100)
y1 = np.sin(x)#Data number 1 associated to checkbox A1
y2 = np.cos(x)#Data number 2 associated to checkbox A2
#This function is called whenever the state of checkboxes changes
def todo():
global b1st, b2st, curve1, curve2
if cbx1.isChecked() != b1st:
b1st = cbx1.isChecked()
if cbx1.isChecked():
if curve1 is None:
curve1 = plot.plot(x, y1, pen = 'r')
else:
plot.addItem(curve1)
else:
plot.removeItem(curve1)
if cbx2.isChecked() != b2st:
b2st = cbx2.isChecked()
if cbx2.isChecked():
if curve2 is None:
curve2 = plot.plot(x, y2, pen = 'y')
else:
plot.addItem(curve2)
else:
plot.removeItem(curve2)
#A widget to hold all of my future widgets
widget_holder = QtGui.QWidget()
#Checkboxes named A1 and A2
cbx1 = QtWidgets.QCheckBox()
cbx1.setText('A1')
cbx1.stateChanged.connect(todo)
b1st = False
curve1 = None
cbx2 = QtWidgets.QCheckBox()
cbx2.setText('A2')
cbx2.stateChanged.connect(todo)
b2st = False
curve2 = None
#Making a pyqtgraph plot widget
plot = pg.PlotWidget()
#Setting the layout
layout = QtGui.QGridLayout()
widget_holder.setLayout(layout)
#Adding the widgets to the layout
layout.addWidget(cbx1, 0,0)
layout.addWidget(cbx2, 0, 1)
layout.addWidget(plot, 1,0, 3,1)
widget_holder.adjustSize()
widget_holder.show()
sys.exit(app.exec_())
I am trying to create a widget callback function that resets the entire plot to its initialized state but it is not working. I expect the users to click Sample as many times as they want then be able to reset the vbar plot to its initialized state.
I have already created the python callback function and used some print functions to debug a bit but the plot is not resetting.
plot2 = figure(plot_height=400, plot_width=int(1.618*600), title="Block Party",
tools="crosshair,reset,save",
x_range=[0, 11], y_range=[0, max(counts)])
plot2.vbar(x='x', top='y', source=source2, width=0.8)
"""
Set up widgets
"""
title2 = TextInput(title="Plot Title", value='Blocks')
sample = Button(label="Sample", button_type="success")
reset = Button(label="Reset", button_type="success")
# Callback
def reset_window_2():
global source2
print("I was clicked")
np.random.seed(42)
unique, counts = np.unique(np.random.randint(low=1, high=11, size=100), return_counts=True)
source2 = ColumnDataSource(data=dict(x=unique, y=counts))
plot2 = figure(plot_height=400, plot_width=int(1.618 * 600), title="Block Party",
tools="crosshair,reset,save",
x_range=[0, 11], y_range=[0, max(counts)])
plot2.vbar(x='x', top='y', source=source2, width=0.618)
reset.js_on_click(CustomJS(args=dict(p=plot2), code="""
plots2.reset.emit()
"""))
print("Check 2")
reset.on_click(reset_window_2)
# Set up layouts and add to document
inputs1 = column(title1, sigma, mu)
inputs2 = column(title2, sample, reset)
tab1 = row(inputs1, plot1, width=int(phi*400))
tab2 = row(inputs2, plot2, width=int(phi*400))
tab1 = Panel(child=tab1, title="Like a Gauss")
tab2 = Panel(child=tab2, title="Sampling")
tabs = Tabs(tabs=[tab1, tab2])
curdoc().add_root(tabs)
curdoc().title = "Sample Dash"
The print functions occur but the reset does not. Any ideas on how to reset the entire plot to init?
Bokeh plots don't show up merely by virtue of being created. In Bokeh server apps, they have to be put in a layout and added to curdoc. You presumably did this:
curdoc.add_root(plot2)
If you want to replace plot2 in the browser, it has to be replaced in curdoc. The plot2 you create in your callback is just a local variable in a function. It pops into existence for the duration of the function, only exists inside the function, then gets thrown away when the function ends. You haven't actually done anything with it. To actually replace in curdoc, it will be easier to store the plot in an explicit layout:
lauyot = row(plot)
curdoc().add_root(layout)
Then in your callback, you can replace what is in the layout:
layout.children[0] = new_plot
All that said, I would actually advise against doing things this way. The general, always-applicable best-practice for Bokeh is:
Always make the smallest change possible.
A Bokeh plot has dozen of sub-components (ranges, axes, glyphs, data sources, tools, ...) Swapping out an entire plot is a very heavyweight operation Instead, what you should do, is just update the data source for the plot you already have, to restore the data it started with:
source2.data = original_data_dict # NOTE: set from plain python dict
That will restore the bars to their original state, making the smallest change possible. This is the usage Bokeh has been optimized for, both in terms of efficient internal implementation, as well as efficient APIs for coding.
I'm trying to create a plot layout using PyQtGraph within a PyQt application.
I need a single row with two plots the first two columns wide and the second a single column wide.
Reading the docs I presumed something like this would work:
# Create the PyQtGraph Plot area
self.view = pg.GraphicsLayoutWidget()
self.w1 = self.view.addPlot(row=1, col=1, colspan=2, title = 'Data1')
self.w2 = self.view.addPlot(row=1, col=3, colspan=1, title = 'Data2')
But in this case I get two plot areas each taking 50% of the window width.
What am I doing wrong?
Best Regards,
Ben
colspan allows you to let a cell in the grid layout span over multiple columns. I a way it merges multiple grid cells. In your example you end up with a grid of 1 row by 3 columns. The first two columns apparently each have a width of 25% of the total (or one has 0% and the other 50%), and the third column takes the other 50%. In short: colspan does not allow you to control the width of the columns.
So, how to set the width of the columns or their contents? That was surprisingly hard to find. There seem to be no PyQtGraph methods that handle this directly, you must use the underlying Qt classes.
A pg.GraphicsLayoutWidget has as its central item a pg.GraphicsLayout. This in turn has a layout member that contains a Qt QGraphicsGridLayout. This allows you to manipulate the column widths with: setColumnFixedWidth, setColumnMaximimumWidth, setColumnStretchFactor, etc. Something like this may be what you need:
self.view = pg.GraphicsLayoutWidget()
self.w1 = self.view.addPlot(row=0, col=0, title = 'Data1')
self.w2 = self.view.addPlot(row=0, col=1, title = 'Data2')
qGraphicsGridLayout = self.view.ci.layout
qGraphicsGridLayout.setColumnStretchFactor(0, 2)
qGraphicsGridLayout.setColumnStretchFactor(1, 1)
Take a look in the documentation of QGraphicsGridLayout and experiment a bit.
I need to Solve two problems With my widget above.
I'd like to be able to define the amount of space put between the post widgets shown in the image (they look fine as is, but I wanna know it's done).
I'd like to grow the text edits vertically based on the amount of text they contain without growing horizontally.
For 1 the code that populates the widgets is as follows :
self._body_frame = QWidget()
self._body_frame.setMinimumWidth(750)
self._body_layout = QVBoxLayout()
self._body_layout.setSpacing(0)
self._post_widgets = []
for i in range(self._posts_per_page):
pw = PostWidget()
self._post_widgets.append(pw)
self._body_layout.addWidget(pw)
self._body_frame.setLayout(self._body_layout)
SetSpacing(0) doesn't bring things any closer, however SetSpacing(100) does increase it.
edit
(for Question 2) I haven't mentioned this, but I want the parent widget to have a vertical scrollbar.
I have answered my own question, but its wordy, and cause and affect based. A proper well written tutorial style answer to address both points gets the bounty :D
edit 2
Using my own answer below I have solved the problem. I'll be accepting my own answer now.
1) Layouts
The other answer on here is very unclear and possibly off about how layout margins work. Its actually very straightforward.
Layouts have content margins
Widgets have content margins
Both of these define a padding around what they contain. A margin setting of 2 on a layout means 2 pixels of padding on all sides. If you have parent-child widgets and layouts, which is always the case when you compose your UI, each object can specific margins which take effect individually. That is... a parent layout specifying a margin of 2, with a child layout specifying a margin of 2, will effectively have 4 pixels of margin being displayed (obviously with some frame drawing in between if the widget has a frame.
A simple layout example illustrates this:
w = QtGui.QWidget()
w.resize(600,400)
layout = QtGui.QVBoxLayout(w)
layout.setMargin(10)
frame = QtGui.QFrame()
frame.setFrameShape(frame.Box)
layout.addWidget(frame)
layout2 = QtGui.QVBoxLayout(frame)
layout2.setMargin(20)
frame2 = QtGui.QFrame()
frame2.setFrameShape(frame2.Box)
layout2.addWidget(frame2)
You can see that the top level margin is 10 on each side, and the child layout is 20 on each side. Nothing really complicated in terms of math.
Margin can also be specified on a per-side basis:
# left: 20, top: 0, right: 20, bottom: 0
layout.setContentsMargins(20,0,20,0)
There is also the option of setting spacing on a layout. Spacing is the pixel amount that is placed between each child of the layout. Setting it to 0 means they are right up against each other. Spacing is a feature of the layout, while margin is a feature of the entire object. A layout can have margin around it, and also spacing between its children. And, the children of the widget can have their own margins which are part of their individual displays.
layout.setSpacing(10) # 10 pixels between each layout item
2) Auto-Resizing QTextEdit
Now for the second part of your question. There are a few ways to create a auto-resizing QTextEdit I am sure. But one way to approach it is to watch for content changes in the document, and then adjust the widget based on the document height:
class Window(QtGui.QDialog):
def __init__(self):
super(Window, self).__init__()
self.resize(600,400)
self.mainLayout = QtGui.QVBoxLayout(self)
self.mainLayout.setMargin(10)
self.scroll = QtGui.QScrollArea()
self.scroll.setWidgetResizable(True)
self.scroll.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn)
self.mainLayout.addWidget(self.scroll)
scrollContents = QtGui.QWidget()
self.scroll.setWidget(scrollContents)
self.textLayout = QtGui.QVBoxLayout(scrollContents)
self.textLayout.setMargin(10)
for _ in xrange(5):
text = GrowingTextEdit()
text.setMinimumHeight(50)
self.textLayout.addWidget(text)
class GrowingTextEdit(QtGui.QTextEdit):
def __init__(self, *args, **kwargs):
super(GrowingTextEdit, self).__init__(*args, **kwargs)
self.document().contentsChanged.connect(self.sizeChange)
self.heightMin = 0
self.heightMax = 65000
def sizeChange(self):
docHeight = self.document().size().height()
if self.heightMin <= docHeight <= self.heightMax:
self.setMinimumHeight(docHeight)
I subclassed QTextEdit -> GrowingTextEdit, and connected the signal emitted from its document to a slot sizeChange that checks the document height. I also included a heightMin and heightMax attribute to let you specify how large or small its allowed to autogrow. If you try this out, you will see that as you type into the box, the widget will start to resize itself, and also shrink back when you remove lines. You can also turn off the scrollbars if you want. Right now each text edit has its own bars, in addition to the parent scroll area. Also, I think you could add a small pad value to the docHeight so that it expands just enough to not show scrollbars for the content.
This approach is not really low level. It uses the commonly exposed signals and child members of the widget for you to receive notifications of state changes. Its pretty common to make use of the signals for extending functionality of existing widgets.
To Address Question 1:
Parent Widgets and Layouts both have margins, in addition to the spacing parameter of the layout itself. From some cause and affect testing It is apprent that margins apply both to the outer region of a parent as well as an internal region.
So, for example if a 2 pixel margin is specified to a parent the vertical border has <--2 pixel | 2 pixel --> margin in addition to the margins of the layout (A HBoxLayout in this case). If the layout has a 2 pixel margin as well the area around horizontal line would look like:
<-- 2 pixel | 2 pixel --> <-- 2 pixel (L) 2 pixel--> (W)
edit Perhaps its more like this: | 2 pixel --> (L) 2 pixel --> <-- 2 pixel (W)
Where | is the vertical line of the parent (L) is the vertical line of the Layout and (W) is the border of the embedded widget in the horizontal layout.
The spacing of the layout is an additional parameter that controls how much space is inserted between widgets of the layout in addition to any layout margins.
The description above might not be accurate( so feel free to edit it where it is inaccurate), but setting the margins of the parent and the layout to zero as well as the layouts spacing to zero produces the result you are after.
For point 2:
I do not think there is a straight forward way to address this issue (you probably have to resort to hooking in at a lower level, which requires a deeper understanding of the API). I think you should use the QLabel Widget instead of the QTextEdit widget. Labels do not have a view and thus expand as needed, at least thats how they work by default, as long as the parent isn't constrained in it's movement.
So, change the QTextEdit to Qlabel and add a scrolling view to the parent and everything should work as you want. I have a feeling you chose QTextEdit because of it's background. Research the way HTML works in QT widgets and you might be able to alter the background via HTML.
edit
This widget (excuse the size) is created by the following code on OS X with PyQT:
import sys
from PyQt4 import Qt
class PostMeta(Qt.QWidget):
posted_at_base_text = "<b> Posted At:</b>"
posted_by_base_text = "<b> Posted By:</b>"
def __init__(self):
Qt.QWidget.__init__(self)
self._posted_by_label = Qt.QLabel()
self._posted_at_label = Qt.QLabel()
layout = Qt.QVBoxLayout()
layout.setMargin(0)
layout.setSpacing(5)
layout.addWidget(self._posted_by_label)
layout.addWidget(self._posted_at_label)
layout.addStretch()
self.setLayout(layout)
self._posted_by_label.setText(PostMeta.posted_by_base_text)
self._posted_at_label.setText(PostMeta.posted_at_base_text)
class FramePost(Qt.QFrame):
def __init__(self):
Qt.QFrame.__init__(self)
layout = Qt.QHBoxLayout()
layout.setMargin(10)
self.te = Qt.QLabel()
self.te.setStyleSheet("QLabel { background : rgb(245,245,245) }")
self.te.setFrameStyle( Qt.QFrame.Panel | Qt.QFrame.Sunken)
self.te.setLineWidth(1)
self._post_meta = PostMeta()
layout.addWidget(self._post_meta)
vline = Qt.QFrame()
vline.setFrameShape(Qt.QFrame.VLine)
layout.addWidget(vline)
layout.addWidget(self.te)
self.te.setText(
""" line one
line two
line three
line four
line five
line six
line seven
line eight
line nine
line ten
line eleven
line twelve
line thirteen""")
self.setLayout(layout)
self.setFrameStyle(Qt.QFrame.Box)
self.setLineWidth(2)
app = Qt.QApplication(sys.argv)
w = Qt.QWidget()
layout = Qt.QHBoxLayout()
fp = FramePost()
layout.addWidget(fp)
w.setLayout(layout)
w.show()
app.exec_()
The labels in the left widget show the spacer and margin tweaking done, and I've used a QLabel for the post text. Notice I've tweaked the label to look a bit more like a default QTextEdit